Kuma is early and maintained by one person.
Three things here are worth reading even if you never report a bug: what naming a package in your declaration opts you into, how to check that the binary you downloaded came from this project, and what happens when the signing key is lost or rotated.
Use GitHub's private vulnerability reporting: Report a vulnerability. It reaches the maintainer privately, and it is the only channel. Please don't open a public issue for one first.
Worth including: the output of kuma --version, the declaration that
reproduces it with any password hash removed, and what an attacker ends up
able to do.
Expect a reply in days rather than hours. There is no bounty and no response guarantee. Only the most recent release is supported; fixes go out in a new release rather than as backports to older tags.
Kuma compiles a declaration into a Containerfile and hands it to podman. It builds no packages and no kernels. What it adds to an image is its own binary, the systemd units it writes, and the desktop assets compiled into it.
So a flaw in the kernel, in systemd, in a Fedora package, or in bootc is
not kuma's to patch. kuma has no patch mechanism of its own because it
needs none: a rebuild resolves against Fedora's packages as they are that
day, which makes updating and patching the same operation. kuma update
is how you take security updates.
A flaw in how kuma generates a build, in what it puts in an image, or in what it runs on your machine is kuma's, and is worth reporting.
One short file spans several trust roots, and naming a string is how you opt into each:
packages.rpmcomes from Fedora's repositories. You cannot declare a third-party repository; there is no key for it. Signature checking is dnf's default and kuma never disables it. A name that tries to become a flag (rpm = ["--nogpgcheck"]) is rejected before it reaches dnf.system.base, when set, is trust in whoever publishes that image. Unset, kuma composes a base from Fedora's repositories instead, so the trust root is the same as forpackages.rpm.packages.flatpakis trust in Flathub and in each application's publisher. These converge on every boot, as root.packages.brewis trust in Homebrew and in each formula's upstream. Naming any formula makes the image fetch Homebrew's tarball over HTTPS on first boot, with no signature to check because Homebrew publishes none. Formulae install into/home/linuxbrew, owned by your user, rather than into the image.services.enablestarts units that are already in the image. It cannot introduce one.system.ca_certificatesis trust in a certificate authority, and it is the most direct entry on this list: a certificate named there is trusted for every TLS connection the machine makes.kuma checkrejects a value that is not a PEM certificate, and rejects one containing a private key outright rather than warning, because a key there would be baked world-readable into every image built from that declaration.
Names in these lists are validated before they reach dnf, flatpak,
systemctl, or brew: no leading dashes, so a name can't become a flag, and
no shell metacharacters. rpm = ["fish; rm -rf /"] is rejected by kuma check.
Choosing a desktop brings in two package sources beyond Fedora's own. Neither appears in the list above, because neither is something you asked for by name. They are listed here rather than left for you to find in a build log.
- RPM Fusion, on every desktop build. Fedora's
mesa-va-driversships with H.264/H.265/VC-1 decode stripped for patent reasons, so video falls back to the CPU. kuma installs RPM Fusion'smesa-va-drivers-freeworldinstead. Getting there means installingrpmfusion-free-releasefrom a URL, which is the bootstrap every third-party Fedora repository has: the package that carries the signing key cannot itself be checked against it. dnf reports this asskipped OpenPGP checks for 1 package. Everything afterwards, including the driver itself, is checked against RPM Fusion's key. fedora-cisco-openh264, which Fedora enables by default and which reaches the image because the desktop layer installs weak dependencies. It is hosted by Cisco rather than Fedora.
Both are the same trust decision Fedora Workstation makes for the same
reason, and a minimal declaration reaches neither. If you want a
machine that trusts only Fedora, declare no desktop.
A declaration is written to be committed and is baked world-readable
into every image built from it, at /usr/lib/kuma/kuma.toml. Two
[user] strings publish with it:
user.password_hashis readable by anyone who can pull the image, who can then start cracking it offline. That is fine for an image that never leaves your machine and bad for one you publish, so don't push an image built from a declaration that carries one. The committed examples declare no user for this reason.user.ssh_keysholds public keys and is safe to publish.user.autologinmeans the machine boots to a session with no password prompt. It is a deliberate choice for a kiosk or a VM, and a poor one for a laptop that leaves the house.
Every image also runs sshd with password authentication on, and the
account an install creates is in wheel. The firewall does not open ssh
to the world: the only address that reaches it is qemu's user-net
gateway, the lane the test harness arrives from. Online guessing is
capped at 50 failures for a day. What that means on a network you run,
how to open ssh deliberately, and how to turn it off:
the base system.
kuma.lock records what a build resolved. The base digest is enforced,
so the same declaration and the same lock build from the same bytes.
Package versions are recorded but not enforced, because Fedora's mirrors
garbage-collect old builds within weeks and a pinned version would become
a build failure rather than a defense. The record is there to show what
moved between two builds.
kuma update is the only thing that moves the pin.
Every release asset is signed with Sigstore, keyless, using the release workflow's own identity. There is no private key to steal. What an attacker would have to take is push access to this repository.
$ cosign verify-blob \
--bundle kuma-x86_64-unknown-linux-musl.bundle \
--certificate-identity-regexp '^https://github.com/Letdown2491/kumaos/.+@refs/tags/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
kuma-x86_64-unknown-linux-muslWorth doing: the install instructions put this binary in
/usr/local/bin, and it goes on to build the filesystem you boot. The
same command appears in every release's notes. That is deliberate: a
release's notes cannot be corrected once people have them, so the two
say one thing. The regexp is narrower than it was at 44.0, on purpose:
the workflow also signs every push to main, for the rolling artifact,
and a prefix match alone would verify one of those as a tagged release.
@refs/tags/ is the part a tag-triggered run has and a main run does
not.
The installer media is signed the same way:
$ cosign verify-blob \
--bundle kuma-x86_64.iso.bundle \
--certificate-identity-regexp '^https://github.com/Letdown2491/kumaos/.+@refs/tags/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
kuma-x86_64.isoWorth more, if anything. The binary builds a system you then choose to
boot; this file boots one directly, on hardware, before you have
anything to inspect it with. The .sha256 beside it is not a
substitute, because it is served from the same page: whatever could
replace one could replace both.
Published images are signed with a key pair instead, and cosign.pub in
this repository is the public half:
$ cosign verify --key cosign.pub ghcr.io/letdown2491/kuma:niriThe two differ for a reason. A podman policy.json can name either a
key or a Sigstore identity, but a GitHub Actions certificate carries no
email, and the identity form of the policy requires one. So no policy
file can express "signed by kuma's release workflow". An image a machine
trusts needs a key the policy can name; a blob a person verifies once by
hand does not.
Your machine checks that signature without being asked. Every kuma
image ships the key at /etc/pki/containers/kuma.pub and a
/etc/containers/policy.json requiring a valid kuma signature for
ghcr.io/letdown2491/kuma, so an update that did not come from kuma is
refused rather than installed. kuma doctor checks this, because a
signature nobody checks is a claim, not a control.
The rule is deliberately narrow. That policy file is shared by podman
and bootc, so requiring signatures everywhere would refuse Fedora's base
image on your next kuma update and refuse your own locally built image
on your next kuma switch. Everything other than kuma's own published
repository is left as it was.
Two consequences. The identity is matched at repository level, so any image kuma signed and published is trusted by any machine tracking that repository, whatever tag it pulls. And images you build yourself are not signed and not required to be; they come from your own storage, which the policy leaves alone.
Publishing is refused without a signature unless somebody explicitly
asks for it, and the workflow verifies its own output against the
committed public key before finishing. That check exists because a
mismatched cosign.pub fails silently: the image publishes, and
verification fails everywhere else.
The private half of the key lives in GitHub's secret store; only the public half is committed, and the check above is what ties the two together. What happens to that key is worth saying plainly:
Losing it costs what is unpublished, nothing published. Signatures live in the registry, so machines keep verifying and keep upgrading within what has already been published. Nothing new can be signed. And a machine whose policy names the lost key cannot receive a new policy by update, because the update is exactly what that policy refuses. Adopting a new key is a deliberate step on each machine. That is the design refusing to make key adoption silent.
Rotating while the key is held can ride an update. Images can be signed with both keys while the policy names both, so the new policy reaches machines as an ordinary verified update, and the old key retires once no machine still requires it.
Rotating because the key is compromised has no in-band path, by design. Any path that swapped a machine's key on its own is the path an attacker holding the key would use too. So a new key reaches machines the way the first one did: deliberately, by the person responsible for the machine.
Images kuma builds for you are not signed. They are built on your machine, from your declaration, and stay in your local container storage unless you push them somewhere. What these guarantees promise, and what a release that ends a promise may change, is stated in the contract.
The [nostr] layer's signing key is a different custody problem: it
lives on the machine it signs for, in your login keyring. Where it sits,
what the lock does and does not protect, and what a paired app can do:
the nostr layer.
init, check, generate, and build need only rootless podman.
switch, update, rollback, and sync call bootc and systemctl
under sudo. vm and iso need sudo because bootc-image-builder runs as
root, with one exception: iso --live never calls it, and so never
asks. install runs bootc install in a privileged container with
/dev bound in, which is what writing a disk requires. Kuma asks for
sudo at those points and nowhere else.
- Builds are not reproducible, and kuma makes no claim that two builds of one declaration produce identical bytes.
- Kuma emits no SBOM.
kuma.lockrecords resolved package versions, which is adjacent but not the same thing. - Images built from your own declaration are not signed and are not
required to be; they come from your own storage, which the policy
leaves alone. The base image is unsigned, and the policy deliberately
leaves it alone too: the digest pin in
kuma.lockis the only check on it. - There is no security advisory history, because there have been no advisories.