Building a FreeIPA-Ready COSMIC Desktop with Fedora Atomic

A custom Fedora Atomic bootc image built on the COSMIC desktop, pre-wired for FreeIPA domain enrollment and developer tooling — and the three traps encountered building it.

Building a FreeIPA-Ready COSMIC Desktop with Fedora Atomic
Photo by Aperture Vintage / Unsplash

If you run a home lab with FreeIPA for centralized authentication, you already know the friction: a new desktop machine means another round of ipa-client-install, another manual Flatpak setup, another afternoon of making things just work. What you want is a machine image you can deploy and have it already be right.

This is the story of building exactly that — a custom Fedora Atomic (bootc) image on top of the COSMIC desktop, pre-wired for FreeIPA domain enrollment, developer tooling, and daily use. And it's also a story about the things that went wrong building it, because the interesting parts of any systems project are the traps you didn't see coming.


The Stack

Fedora Atomic images are immutable OS deployments built on OSTree: the system partition is read-only, updates are atomic (you reboot into a new tree or stay on the old one), and customization happens at image-build time rather than post-install. The bootc project extends this to OCI container images — your OS is literally a container that you can build with a Containerfile, push to a registry, and deploy anywhere.

COSMIC is System76's new Rust-written desktop environment, now shipping in Fedora as a first-class variant: quay.io/fedora-ostree-desktops/cosmic-atomic. It's worth tracking if you want a modern, GPU-accelerated Wayland desktop that isn't GNOME or KDE.

The goal: take that COSMIC base image and layer on everything a home lab user needs without ever having to touch the system after deployment.

The image includes:

  • FreeIPA clientfreeipa-client, krb5-workstation, oddjob-mkhomedir — domain enrollment ready
  • Homebrew — available to all users without per-user installs
  • Flathub + defaults — Firefox, Thunderbird, Flatseal, and more, on first boot
  • RPM Fusion — full ffmpeg + hardware video decode via VA-API
  • Tailscale — installed and enabled; run tailscale up to connect
  • ujust recipes — one-command IPA enrollment, system updates, cleanup

How the Build Works

The image is assembled from three pieces, wired together by a Containerfile.

system_files/ is a literal directory overlay onto /. Whatever exists at a path in this folder becomes that file in the image. Systemd units, profile scripts, helper scripts, /etc defaults — all drop in here. No post-install scripts, no Ansible, just files.

build_files/build.sh is the only place packages get installed. It runs as a single RUN step with set -euxo pipefail, enables non-default repos (RPM Fusion, Tailscale, two COPRs), installs everything in one dnf install transaction, sets up Homebrew, configures firewalld, and ends with ostree container commit.

Containerfile pulls the COSMIC Atomic base image, copies system_files/ onto /, then runs the build script. That's it. The resulting image is pushed to GHCR on every push to main and on a weekly schedule to pick up upstream package updates.

FROM quay.io/fedora-ostree-desktops/cosmic-atomic:44

COPY system_files/ /

COPY build_files/build.sh /tmp/build.sh
RUN chmod +x /tmp/build.sh && /tmp/build.sh

The Traps

Three things bit hard enough to be worth writing about. None of them are obvious until you've already hit them.

The /var exclusion

⚠️ Challenge: Flatpak was configured at build time. Deployed machines booted with no Flathub remote and no apps installed — despite a successful build that registered the remote and installed seven applications.

On Fedora Atomic, ostree container commit includes /usr and /etc in the image. /var is excluded entirely — treated like a Docker VOLUME, populated fresh at first boot from a seed, but never committed to the image. Flatpak's system installation lives at /var/lib/flatpak. So flatpak remote-add --system and flatpak install run fine during the build, write their state to /var, and are then silently discarded when ostree container commit runs. The deployed machine boots with no Flathub and no apps.

💡 Fix: A systemd oneshot service, flathub-setup.service, that runs flatpak remote-add and flatpak install at first boot when /var is real. The service is idempotent and protected by Wants=network-online.target. This is the right pattern for anything whose state lives under /var — Homebrew uses the same approach.
⚠️ Challenge: Homebrew's installer failed with a confusing error: mkdir: cannot create directory '/root': File exists — even though /root clearly existed.

Fedora Atomic ships /root → /var/roothome and /home → /var/home as symlinks. The symlinks exist at build time, but the targets don't — they're created at first boot when /var is populated. Homebrew's installer writes to $HOME/.cache. During the build, $HOME resolves to /root, which is a symlink pointing at /var/roothome — which doesn't exist yet. Trying to write there fails in exactly this confusing way.

The fix is two lines at the top of build.sh:

# /root → /var/roothome and /home → /var/home are symlinks whose
# targets don't exist until first boot. Pre-create them.
mkdir -p /var/roothome
mkdir -p /var/home

This is the kind of thing that's obvious in retrospect and completely invisible until you hit it.

The cosmic-greeter bug

⚠️ Challenge: FreeIPA enrollment succeeded. SSSD was running and resolving accounts. But typing an IPA username at the COSMIC login screen silently opened a session for a different local account instead, with no error shown.

This one took the longest to diagnose because the symptoms looked like an SSSD or HBAC configuration problem. The checklist was exhaustive: SSSD enumeration, HBAC rules (allow_all, service category: all), IPA ID ranges (the server assigned UIDs in the billions — valid, NSS resolved them fine). None of it was the issue.

The real diagnostic was a journalctl trace during a live failed login:

journalctl -u cosmic-greeter -u cosmic-greeter-daemon -f

No pam_sss line appeared anywhere in the log. The greeter never submitted the typed username to PAM at all. It's a confirmed upstream bug in pop-os/cosmic-greeter#376: a manually-typed username that isn't in the greeter's enumerated user list gets silently dropped instead of forwarded to PAM/SSSD.

💡 Fix: Swap cosmic-greeter for GDM. GDM correctly forwards any typed username to PAM/SSSD and still lists the COSMIC session in its session switcher — so you keep the COSMIC desktop, just not its login screen. A ujust toggle-login-manager recipe lets you switch back once the upstream bug is patched.

One clarification worth noting: the lock screen is a separate question. COSMIC's lock screen is handled by cosmic-comp (the compositor) via the ext-session-lock-v1 Wayland protocol — GDM is not involved. IPA password authentication at the lock screen goes through pam_sss normally.


Homebrew on an Immutable OS

Homebrew wants to live in a writable prefix. Fedora Atomic's /usr is read-only. The solution turns Homebrew into a first-boot artifact.

During the container build, Homebrew is installed normally into /var/home/linuxbrew/.linuxbrew (after pre-creating the path — see above). Once installed, the entire prefix is tarred and compressed into /usr/share/homebrew.tar.zst, which becomes part of the image. The live copy under /var is then deleted (since it won't survive ostree container commit anyway).

touch /.dockerenv            # installer checks for container environment
NONINTERACTIVE=1 /tmp/brew-install.sh
tar --zstd -cf /usr/share/homebrew.tar.zst -C /var/home linuxbrew
rm -rf /.dockerenv /var/home/linuxbrew

On first boot, brew-setup.service unpacks the tarball into the real /var/home/linuxbrew and sets ownership to UID 1000 (the primary user). Every user gets brew on their PATH via /etc/profile.d/brew.sh — only the owner can install or update packages, but everyone can run them.

The tarball approach means Homebrew is always at the build's version on first boot. After that it lives in /var, which persists across image updates — a bootc upgrade doesn't reset your Homebrew packages.


FreeIPA Enrollment in Practice

With the packages baked in and GDM handling authentication, enrollment is a single ujust recipe:

ujust ipa-enroll

The recipe prompts for a domain and optional server, runs ipa-client-install --mkhomedir, then restarts sssd, oddjobd, and certmonger in place — avoiding a reboot in most cases. ipa-client-install rewrites PAM, NSS, sssd.conf, and krb5.conf, but already-running services cache the old config; the restart flushes that.

The recipe also sets enumerate = True in sssd.conf. With GDM this isn't required for login to work — GDM accepts any typed username. But if you later switch to cosmic-greeter once it's fixed, enumeration is what makes IPA accounts appear in its user list rather than needing to be typed.


RPM Fusion and Mirror Reliability

The build installs codec and hardware acceleration packages from RPM Fusion. In CI, this surfaced as a pattern of sporadic build failures when mirrors.rpmfusion.org (a redirector that hands out a different mirror per request) returned a slow or unresponsive mirror.

dnf's retry logic retries across mirrors but through the same redirector call, which can land on the same bad mirror repeatedly. The fix: download the RPM Fusion release RPMs via curl first, install them locally, then run dnf install. This bounds each download attempt with both a connect timeout and a total-time limit:

curl --retry 5 --retry-all-errors --retry-delay 3 \
     --connect-timeout 15 --max-time 40 -fsSL \
     "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm" \
     -o /tmp/rpmfusion-free-release.rpm
dnf -y install /tmp/rpmfusion-free-release.rpm

--max-time 40 is the key addition: --connect-timeout only bounds the TCP handshake, not the transfer. A mirror that accepts the connection and then drips data at 1 KB/s would otherwise hang the build indefinitely.


The Result

Deploying to a machine is a single command:

sudo bootc switch ghcr.io/nativetexan70/fedora-atomic-cosmic:latest
sudo reboot

After the reboot, Homebrew and Flathub set themselves up in the background. ujust ipa-enroll handles domain membership. The full terminal setup — Starship prompt, eza as ls, bat as cat, fastfetch on shell open — is live for every user. VA-API hardware acceleration works out of the box on Intel. Tailscale needs one tailscale up.

The image rebuilds weekly, so packages and the base COSMIC image stay current without any manual effort. ujust update covers everything else: the base image, Flatpaks, and Homebrew in one shot.

The source is at github.com/nativetexan70/fedora-atomic-cosmic. The docs/ folder has detailed pages on each feature area — FreeIPA integration, the login manager rationale, Homebrew ownership, VA-API troubleshooting, and more.