On the first boot of Fedora CoreOS, rpm-ostree applies qemu-guest-agent
live and runs "systemd-tmpfiles --create --prefix=/var" after the
containers have started. The "d" line reset the SELinux label of the
directory Podman had relabeled, and every REGISTER then failed with
"ast_db_put: SQL logic error". The package now creates the directory and
its spool and log sub-directories, once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJKweojs6NK5svZwRgQjQH
Asterisk 22 (LTS) and dnsmasq from the Alpine 3.24 packages, in one image
built locally. Asterisk runs as UID 10036 with PJSIP only and an explicit
module list; dnsmasq serves the provisioning files of the phones over TFTP
from the virtiofs mount, where the host can write firmware images that do
not fit in an Ignition file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJKweojs6NK5svZwRgQjQH
- coreos: everything specific to Fedora CoreOS (virtiofs discovery and
mount, qemu-guest-agent, SSH host key persistence, DNS configuration).
- base: everything else (install-tools, fastfetch, resolv.conf resync,
base images, users and groups).
common.mk now injects both as implicit dependencies: coreos has none,
base depends on coreos and every other project depends on coreos and base.
default-butane-spec.sh skips ignition files whose content duplicates a
previous one: butane rejects duplicate merge entries, which happens with
empty "-examples" ignition files (e.g. coreos-examples and nftables-examples).
The virtiofs tags are now discovered in /run/quadlets/coreos/virtiofs
(seedbox mounts updated accordingly) and the tmpfiles.d file is named
coreos.conf so that make install/uninstall picks it up.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
qBittorrent has no /health endpoint (404) and its WebAPI requires
authentication (403), so probe the WebUI root instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMEYncwy1yEBJCSb8HLgXg
- Use the "default" certificate ID in the examples and documentation.
- Hooks use try-restart so that they do not start the service early on the
first issuance; the Quay hook skips the restart when no certificate has
been loaded yet.
- quay-app is ordered after lego.target.
- Document a renewal hook for Traefik.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMEYncwy1yEBJCSb8HLgXg
- ExecStartPre copies the lego certificates into the tmpfs at each start, so
that vsftpd serves TLS after a reboot and after the first issuance.
- The lego hook only try-restarts vsftpd.
- TLS settings (TLS 1.3, default.crt) move to the local.conf and
virtual-users.conf examples; tls.conf is removed.
- The shared storage is mounted through a drop-in instead of being hardcoded.
- Add pytest integration tests (FTPS against Pebble, renewal, reboot).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMEYncwy1yEBJCSb8HLgXg
- A single oneshot lego.service (lego --config /etc/lego/lego.yaml) obtains
and renews the certificates, triggered at boot and daily by lego.timer.
- The deploy hook flags the certificate as renewed; lego-hooks.path then runs
renew-hooks.d/*.sh on the host, on the first issuance and on each renewal.
- lego.target only Wants lego.service: an ACME failure does not stop the
target and the timer retries.
- Consumers rely on the fixed certificate ID "default".
- Dev mode generates a self-signed "default" certificate.
- Document the v4 to v5 upgrade (reissue) and add pytest integration tests
against Pebble.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMEYncwy1yEBJCSb8HLgXg
With Network=host, Podman snapshots /etc/resolv.conf at container creation
and bind-mounts the copy, so a later DNS change (DHCP renewal, renumbering,
router replacement) never reaches running containers: they keep querying the
old, now-dead resolvers until recreated, silently.
Add a host-level mechanism in the base cookbook:
- a systemd.path unit watching /etc/resolv.conf (catches any writer, including
atomic rename-into-place);
- a oneshot service running a dedicated script that rewrites each Network=host
container's snapshot in place (same inode, so the bind-mount stays valid).
Rewriting the content in place is picked up at runtime with no restart by both
glibc (reloads on mtime change) and musl (stateless, re-reads every lookup),
verified empirically. Scoped to NetworkMode=host only: bridged containers
legitimately point at aardvark-dns and must not be touched.
Fixes#1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /dev/vdb -> /var partitioning in cookbooks/base/overlay.bu was merged
into every project's main spec, so it ended up in the production/test
ignition even though the second disk (var.qcow2) only exists in the libvirt
dev/test VM. A real host should not have `base` blindly repartition /dev/vdb.
Move it to local.bu / local.bu.template, which feeds local.ign and is merged
into both fcos-dev.ign and fcos-test.ign but is never part of the shipped
package. This restores partitioning for all VMs while keeping it out of
production.
Also add the overlay-examples.bu merge mechanism to scripts/common.mk (mirror
of the existing overlay.bu merge) for future per-project example overlays, and
whitelist overlay-examples.bu in .gitignore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- home-assistant.container now gates on the live /config/configuration.yaml
instead of the /etc/quadlets bootstrap template: once Home Assistant owns
/config the bootstrap files are no longer needed.
- Move the init unit's inline shell into an external script,
config/init.sh -> /etc/quadlets/home-assistant/init.sh, referenced from
home-assistant-init.service.
- Drop the (non-essential) ConditionPathExists from home-assistant.target;
its members self-gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Podman Quadlet cookbook for Home Assistant.
- Runs as the non-root home-assistant user (UID 10034, GID itix-svc/10000);
s6-overlay v3 self-adjusts /run so no root or idmap workaround is needed.
- PostgreSQL recorder backend (postgresql hook) with the connection string kept
out of packaged files via !secret; Traefik ingress (traefik hook) with the
X-Forwarded-For / trusted_proxies settings a reverse-proxied HA requires.
- /config is precious data on the virtiofs mount; a one-shot init unit bootstraps
configuration.yaml and secrets.yaml into it only when absent (never overwriting
the live copy Home Assistant owns) and creates the empty automations/scenes/
scripts include targets so a fresh /config does not fall into recovery mode.
- Weekly native backup automation writing into /config/backups.
- Health check on /manifest.json with cold-start-sized timeouts.
Verified on a clean Fedora CoreOS VM (real virtiofs): home-assistant.target
active, 8123 answering, recorder writing 13 tables into PostgreSQL, all
containers healthy, and a re-run of the init unit leaving the live config intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `mosquitto` cookbook running Eclipse Mosquitto 2 (multi-arch
docker.io/library/eclipse-mosquitto:2) as a rootless, non-root container
(UID 10033 / GID 10000) serving as the MQTT backbone of a home-automation
stack.
Design highlights:
- Plain MQTT on 127.0.0.1:1883 only; TLS is terminated at Traefik (:443,
SNI-routed) via a TCP router hook fragment (other/traefik/mosquitto.yaml).
Depends on the traefik cookbook.
- Anonymous access refused; password_file and acl_file are operator-provided
(working examples shipped under config/examples/, installed 0640 10033:10000).
No credential is baked into a packaged file.
- Retained messages and sessions persist to the virtiofs precious-data mount
(/var/lib/virtiofs/data/mosquitto), purged on uninstall via a tmpfiles `d$`
entry.
- log_dest stdout so journalctl is the single place to look.
- include_dir conf.d shipped as an empty packaged directory (mosquitto fails if
it is missing); hooks.mk lets dependent cookbooks drop conf.d fragments.
- Health check is an unauthenticated `nc -z 127.0.0.1 1883` TCP connect, so it
needs no secret that may not exist yet.
Verified: make dryrun, whole-repo `make package`, and a podman-level functional
run (non-root startup, loopback-only bind, anonymous refused, authenticated
pub/sub round trip, retained message surviving a restart, health probe).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runs a browser on the seedbox and displays it on the workstation through
waypipe, for the cases FlareSolverr cannot cover: logging into a private
tracker, or reaching an interface only exposed on the seedbox network.
waypipe lives in the container rather than on the Fedora CoreOS host. The
waypipe server has to share a mount namespace with the browser, and putting it
in the image keeps the host a stock, unlayered CoreOS while pinning the waypipe
version -- 0.9 (C) and 0.10+ (Rust) do not interoperate. The entry point is
seedbox-waypipe.sh, passed to the client with --remote-bin, because
"ssh <host> <command>" never sources /etc/profile.d.
LibreWolf because the seedbox is aarch64, which rules out Tor Browser and
Mullvad Browser (no Linux arm64 build). It ships resistFingerprinting, uBlock
Origin and no telemetry out of the box, and disables its own updater, which
suits an image rebuilt nightly by librewolf-build.timer.
Four things that are not obvious from the code:
- SELinux checks "connectto" on a Unix socket against the listening process,
not the socket file, and the listener is the unconfined SSH session. No
label the container could carry is allowed to connect, hence
--security-opt label=disable rather than a system-wide policy module.
- Firefox's content sandbox chroots itself and needs CAP_SYS_CHROOT in the
bounding set, while bubblewrap -- used by the SVG image loader -- refuses to
run when the caller holds capabilities without being setuid. podman puts
--cap-add in the ambient set too, so the image entrypoint drops ambient and
inheritable capabilities with setpriv, leaving the bounding set alone.
- The forwarded socket is bind-mounted by directory, not by file: mounting the
file alone makes podman synthesize a root-owned 0755 parent inside the
container, and waypipe then cannot unlink its own socket on shutdown.
- "waypipe ssh" forwards several of its own options to the server side, and a
repeated flag is a hard error, so the shim only injects what is absent.
The browser deliberately does not use Network=host, unlike the rest of the
stack: a hostile page must not reach the *arr interfaces on localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kXTkLKMkwYSHZET7ckzmd