If you discover a security vulnerability in fisherman, please report it privately:
- Do not file a public issue. Use GitHub's private vulnerability reporting on this repository (Security → Report a vulnerability).
- Provide details: the affected version or commit, reproduction steps, and the impact.
- Allow time for a fix before any public disclosure. Fixes land on
devand reach users through the normal release flow, and then through each frontend's submodule bump.
Reporters who disclose responsibly will be credited in the advisory unless they ask not to be.
fisherman runs as root, partitions and formats whole block devices, and creates LUKS volumes. It assumes a trusted host, trusted container images and a trusted caller.
- Validation before disk changes. Every rule
fisherman validateenforces runs inside the install itself, before any disk is touched; the install refuses an invalid recipe (seedocs/VALIDATE.md). The host tools the recipe needs are also checked before partitioning. - Encryption as requested. When LUKS is requested, fisherman formats the
root volume with the recipe's passphrase, or, for
tpm2-luks, a random recovery passphrase that it emits for the user to save. TPM2 is enrolled on the installed system's first boot, not during the install, because PCR 7 measured in the installer differs from the installed system's. - No unlock key outlives its use. The first-boot enrolment needs a key.
fisherman never stages the user's passphrase or the recovery key for it. It
adds a random key in its own LUKS keyslot (slot 31) and stages only that
key, in the installed deployment's
/etc/fisherman/tpm2-enroll.key(mode0600, directory0700), inside the encrypted root. The first-boot unit (fisherman-tpm2-enroll.service, in the deployment's/etc/systemd/system) tries the enrolment at most three times in that one boot. When it stops, it removes the keyslot and then the file. This runs inExecStopPost=, so it also runs when the enrolment fails, times out or the tool is missing. After that the file's old blocks unlock nothing, on any filesystem. There is no retry on a later boot. fisherman looks forsystemd-cryptenrollin the installed image (an ostree deployment), and adds no keyslot and stages nothing when the image has none (fisherman/internal/luks/firstboot.go). - Host secrets stay on the host unless the user is on live media.
NetworkManager profiles (
/etc/NetworkManager/system-connections: Wi-Fi passwords, 802.1X credentials) and Bluetooth pairings (/var/lib/bluetooth: link keys) are copied to the target only when the host booted from live media (thelive.is_liveoffisherman probe), where they are what the user set up in the installer session. On any other host fisherman copies neither, because the target can be for another person. The recipe fieldscopyNetworkProfilesandcopyBluetoothPairingsoverride the default in both directions. The log says for each kind what was copied or why not. - Scratch and mount cleanup. Mounts, LUKS mappings and scratch directories fisherman creates are released in dependency order, including on the fatal-error path. Scratch that lives on the target disk (live media) is deleted while the target is still mounted read-write, before finalize and before any unmount, so no image cache stays on the installed disk.
- An unauthorised caller. fisherman checks that a recipe is well formed, but not who wrote it or whether the caller was entitled to run it. Anyone who can run fisherman as root can erase any disk on the host. Authorisation is the job of the escalation layer (pkexec policy, sudoers) and of the frontend.
- A compromised host. If the host, its container runtime or its system tools are already compromised, so is the installed system.
- A malicious image. The image is installed and then booted; fisherman does not inspect its contents. Image authenticity relies on the host's containers configuration (registry TLS and any signature policy).
- Data on a disk that has been installed over. A failed install may already have repartitioned or formatted the target disk; cleanup releases mounts and mappings, it does not restore the previous contents.
- Parallel installs on one host. They are refused, not isolated. An
install takes a host-wide lock (
flockon/run/lock/fisherman-install.lock) before it reads its recipe, and a second install ends with the codeinstall_already_running. The kernel releases the lock when the process ends, however it ends. SettingtargetMountandluksMapperNamein the recipes is not enough to run two installs side by side: both still use the scratch directory/var/fisherman-tmpand bind it over the host's/var/tmp, and each teardown removes that directory. The two fields remain for a host where the default paths are taken. The lock does not stop another program, or a second host that shares the disk, from touching the target.
These behaviours have security implications and must not be simplified or moved.
- On a normal host the scratch directory is
/var/fisherman-tmp(fisherman/cmd/fisherman/main.go), created mode0700. Do not move it under/run/*: that is tmpfs and too small for image blobs. - On live media whose
/varis tmpfs or overlayfs, scratch lives on the target disk at<target mount>/.fisherman-scratchinstead. It is self-bind-mounted, and its removal is registered as a post-removal so it runs after the unmount and LUKS close, and still runs on the error path.
BuildSelinuxBypassShim (fisherman/internal/install/bootc.go) compiles a
small shared library as root, and that library is then LD_PRELOADed into the
privileged bootc container. It must be built in a fresh directory created by
os.MkdirTemp (mode 0700, unpredictable name), never at a fixed path in a
world-writable directory such as /tmp. A fixed path would let an
unprivileged local user pre-plant the library for root to preload, or symlink
the source path at an arbitrary file for root to truncate (fixed in #161).
- GRUB images always use 3 partitions (EFI, ext4
/boot, root), even unencrypted. GRUB cannot read modern XFS features, so/bootis ext4 on its own partition. This is a boot requirement, not an optimisation. - systemd-boot images (dakota) use 2 partitions (EFI, root), with the kernel and initrd on the ESP.
- The ESP is 2 GiB across the whole fleet.
See the partition-layout table in the README.
- Validation.
fisherman validate [--json]runs the same rules as the install, and frontends should call it to show errors early. The stable error codes are listed indocs/VALIDATE.md. - Empty image. An empty
imageis rejected unless the host booted from live media (fisherman probe --jsonreports this aslive.is_live), where it means "install the running image". This keeps a mistake from silently installing the host's own image. - Recipes on stdin.
fisherman -reads the recipe from stdin (bounded to 1 MiB), so a frontend never has to write a recipe containing a LUKS passphrase or user password to a temporary file.
After fisherman exits 0, integrators can check:
- The partition layout matches the image's boot stack (3 partitions for
GRUB, 2 for systemd-boot):
lsblk /dev/X
- For encrypted installs, that the unlock flow works on first boot.
- The deployed image, once booted:
bootc status
fisherman is started as root by its frontend:
| Path | Escalation | Recipe | Notes |
|---|---|---|---|
sudo fisherman … |
sudo | file or stdin (-) |
CLI and test use |
pkexec fisherman … |
polkit | file or stdin (-) |
needs a polkit policy that allows it |
flatpak-spawn --host pkexec fisherman … |
polkit, from a Flatpak frontend | stdin (-) or a host-visible path |
the sandbox cannot see the host, so recipes and fisherman are staged where root on the host can read them |
fisherman does not authenticate the frontend or its recipe. The frontend is responsible for collecting input, confirming the target disk and image with the user, and only then starting fisherman.
- Go: the
fishermanmodule uses only the Go standard library (fisherman/go.modhas no requirements); CI runsgo vet, the unit tests and golangci-lint. The separatetui/module pins its dependencies intui/go.sum. - bootc runs from the target image via
podman run --privilegedwhen an image is given; on live media it is called directly from the running image. - podman and skopeo handle image pulls and offline image copies, and inherit the host's containers configuration.
- System utilities (
sfdisk,mkfs.*,cryptsetup,mount, …) are taken from the host.
All of these are assumed to come from a trusted source. If any is compromised, so is the resulting install.