- 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>
`make fcos-vm` builds build/fcos-dev.ign but never merged the current
project's own *-examples.ign, even though it was listed as a prerequisite
and staged into butane's -d dir. The generated .bu only referenced what
was passed to default-butane-spec.sh, and -examples was appended to the
dependencies but never to $(PROJECT_NAME). Units gated on config that
ships only as an example (e.g. ConditionPathExists) therefore stayed
inactive on a fresh dev VM.
Introduce a SELF variable so the dev spec includes the project's own
examples while the test spec is left unchanged. base is filtered out of
DEPS for its own build, so base-examples is not duplicated.
Fixes#4
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