[Mirror] GNU Guix Channel with popular packages, system- and home services https://codeberg.org/gofranz/panther
  • Scheme 95.5%
  • Shell 2.3%
  • Tree-sitter Query 2.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-18 19:47:41 +01:00
etc px: news: Announce bichon 2.0.0 migration requirement 2026-08-07 13:32:07 +01:00
px px: thunderbird: Update to v156.0 2026-09-18 19:47:41 +01:00
scripts px: autofirma: Added 2026-09-10 08:17:15 +01:00
.editorconfig Updated readme; added pre-inst-env script for testing 2024-03-31 16:34:15 +01:00
.gitignore initial commit 2023-06-25 16:07:34 +01:00
.guix-authorizations .guix-authorizations: Fix key format and add signing subkey 2026-02-22 08:39:40 +00:00
.guix-channel px: Fix channel news file format 2026-01-30 11:18:15 +07:00
CLAUDE.md px: iota: Update to v1.23.2 2026-05-20 18:08:39 +01:00
claws-mail-theme-path.patch initial commit 2023-06-25 16:07:34 +01:00
CONTRIBUTE_GUIX.md px: iota: Update to v1.23.2 2026-05-20 18:08:39 +01:00
COPYING README: Add notice on upstream contributions 2026-01-20 08:35:56 +07:00
goveebttemplogger-postbuild-sudo-fix.patch goveebttemplogger: v2.20231001.1 2023-11-24 20:56:36 +00:00
pre-inst-env Updated readme; added pre-inst-env script for testing 2024-03-31 16:34:15 +01:00
qtwebengine-text-render.patch initial commit 2023-06-25 16:07:34 +01:00
README.md px: nss-fhs: Add service 2026-09-10 08:15:07 +01:00
scodes-zxing-path.patch initial commit 2023-06-25 16:07:34 +01:00
tarsnap-do-not-use-command-p-in-makefile.patch px: tarsnap: Update patch 2025-11-30 16:19:18 +07:00

My guix channel "panther"

This repository contains GUIX package defintions maintained primarily by Franz Geffke.

Channel Definition

(cons* (channel
        (name 'pantherx)
        (url "https://codeberg.org/gofranz/panther.git")
        ;; Enable signature verification
        (introduction
         (make-channel-introduction
          "54b4056ac571611892c743b65f4c47dc298c49da"
          (openpgp-fingerprint
           "A36A D41E ECC7 A871 1003  5D24 524F EB1A 9D33 C9CB"))))
       %default-channels)

Substitute Server

URL: https://substitutes.guix.gofranz.com

;; Public key
(public-key
 (ecc
  (curve Ed25519)
  (q #0096373009D945F86C75DFE96FC2D21E2F82BA8264CB69180AA4F9D3C45BAA47#)))
# Authorize
sudo guix archive --authorize < /path/to/key.pub

Already configured if using %os-base-services or %os-desktop-services.

Home Services

Bichon

Bichon is a self-hosted email archiver: it syncs IMAP accounts, indexes messages for full-text search, and serves a REST API and web interface. It is not an email client — it can't send, compose, or reply. This runs it as a user daemon, with data under $XDG_DATA_HOME/bichon and the log under $XDG_LOG_HOME.

It reuses the shared bichon-configuration record from (px services mail) — see the system service for the full field list. The encryption password is read from a file you manage, never baked into the store. Differences from the system service:

  • No system user/group — runs as your user; user/group are ignored.
  • Data under XDGroot-dir defaults to $XDG_DATA_HOME/bichon (~/.local/share/bichon) instead of /var/lib/bichon, unless you set it explicitly. The full-text index and blob store nest under it (bichon-indices/, bichon-storage/), and the daemon log goes to $XDG_LOG_HOME/bichon.log (~/.local/var/log/bichon.log). The service creates the data and log directories on activation.
  • Binds localhostbind-ip defaults to 127.0.0.1, not all interfaces. Set it explicitly to expose the daemon on your network.

Encryption password: create the password file before first start (the daemon won't start without it):

install -d -m 700 ~/.config/bichon
printf '%s' "$(openssl rand -base64 48)" > ~/.config/bichon/encrypt-password
chmod 600 ~/.config/bichon/encrypt-password

printf '%s' keeps the trailing newline out so the file is exactly the secret. Back it up — this key encrypts your stored IMAP credentials, and it's write-once.

Usage:

(use-modules (px home services mail)
             (px services mail))

(service home-bichon-service-type
         (bichon-configuration
          (encrypt-password-file "/home/user/.config/bichon/encrypt-password")))

Default credentials: first start creates admin / admin@bichon — change it immediately via the web interface (Settings → Profile).

Service management:

herd start bichon    # Start daemon
herd stop bichon     # Stop daemon
herd status bichon   # Check status

Darkman

Darkman is a framework for managing dark/light mode transitions. It automatically switches themes based on sunrise/sunset times.

Usage:

(use-modules (px home services darkman))

;; Default configuration (uses geoclue2 for location)
(service home-darkman-service-type)

;; With manual coordinates
(service home-darkman-service-type
         (home-darkman-configuration
          (latitude 52.52)
          (longitude 13.405)
          (use-geoclue? #f)))

;; Custom configuration
(service home-darkman-service-type
         (home-darkman-configuration
          (latitude 37.7749)
          (longitude -122.4194)
          (use-geoclue? #f)
          (dbus-server? #t)
          (portal? #f)))

Mode-switching scripts:

Place executable scripts in:

  • ~/.local/share/dark-mode.d/ - Executed when switching to dark mode
  • ~/.local/share/light-mode.d/ - Executed when switching to light mode

Manual control:

darkman get          # Show current mode
darkman set dark     # Switch to dark mode
darkman set light    # Switch to light mode
darkman toggle       # Toggle between modes

Foot Server

Foot server mode runs the foot terminal emulator as a background daemon, allowing fast terminal startup with footclient.

Usage:

(use-modules (px home services foot))

;; Default configuration
(service home-foot-server-service-type)

;; With custom config file
(service home-foot-server-service-type
         (home-foot-server-configuration
          (config-file "/path/to/foot.ini")))

;; With hold option (remain open after child exits)
(service home-foot-server-service-type
         (home-foot-server-configuration
          (hold? #t)))

Connecting to server:

footclient           # Open new terminal window
footclient -- htop   # Open terminal running specific command

Service management:

herd start foot-server    # Start server
herd stop foot-server     # Stop server
herd status foot-server   # Check status

Unattended Upgrade

Periodically runs guix pull followed by guix home reconfigure. Drop-in home equivalent of the system unattended-upgrade-service-type with battery awareness — upgrades are skipped when the laptop is on battery power.

Usage:

(use-modules (px home services unattended-upgrade))

;; Minimal configuration (config-file is required)
(service home-unattended-upgrade-service-type
         (home-unattended-upgrade-configuration
          (config-file "/home/user/dotfiles/home/home.scm")
          (channels #~
                    (cons* (channel
                            (name 'my-channel)
                            (url "https://example.com/channel.git"))
                           %default-channels))))

;; Full configuration
(service home-unattended-upgrade-service-type
         (home-unattended-upgrade-configuration
          (config-file "/home/user/dotfiles/home/home.scm")
          (skip-on-battery? #t)
          (schedule "0 19 * * *")
          (channels #~ %default-channels)))

Configuration options:

Field Default Description
config-file (required) Path to home.scm configuration file
channels %default-channels Gexp producing a list of channels for guix pull -C
schedule "0 19 * * *" Cron schedule string
system-expiration 90 days Max age of home generations before deletion
maximum-duration 3600 Max seconds the upgrade may run
skip-on-battery? #f Skip upgrade when on battery power
log-file ~/.local/state/unattended-home-upgrade.log Log file path
warm-packages '() List of package names to guix build after reconfigure

Service management:

herd status unattended-home-upgrade   # Check status
herd trigger unattended-home-upgrade  # Trigger upgrade now

Podman Healthcheckd

Runs podman container healthchecks on systems without systemd.

Usage:

(use-modules (px services containers))

;; Default configuration
(service home-podman-healthcheckd-service-type)

;; With custom log level
(service home-podman-healthcheckd-service-type
         (home-podman-healthcheckd-configuration
          (log-level "debug")))

Service management:

herd start podman-healthcheckd    # Start daemon
herd stop podman-healthcheckd     # Stop daemon
herd status podman-healthcheckd   # Check status

wayvnc

wayvnc shares a running wlroots-based Wayland session over VNC. It attaches to the compositor you're already using, creates virtual input devices, and serves an output over RFB.

Credentials stay out of the world-readable store. Activation creates ~/.local/state/wayvnc at 0700 and, if they aren't there yet, generates a self-signed secp384r1 certificate and a PKCS#1 RSA key; the config file is written next to them at 0600. The RSA key isn't really optional - neatvnc offers RSA-AES either way, and with no key on disk it makes a fresh one per start, so clients warn about a changed host key on every restart.

Authentication defaults to PAM, which needs the PAM system service. For a fixed password instead, set enable-pam? to #f and point password-file at a file you manage.

Usage:

(use-modules (px home services wayvnc))

;; PAM auth, generated TLS + RSA credentials, listening on localhost
(service home-wayvnc-service-type)

;; Reachable over a Tailscale address, with that address in the certificate
(service home-wayvnc-service-type
         (home-wayvnc-configuration
          (address "100.64.0.5")
          (tls-common-name "desk.tail1234.ts.net")
          (tls-subject-alt-names
           '("DNS:desk.tail1234.ts.net" "IP:100.64.0.5"))))

;; Fixed password instead of PAM
(service home-wayvnc-service-type
         (home-wayvnc-configuration
          (enable-pam? #f)
          (username "franz")
          (password-file "/home/franz/.config/wayvnc/password")))

Whatever address you connect to has to appear in tls-subject-alt-names, or clients that check the certificate refuse it. The default binds localhost; prefer an overlay network address over 0.0.0.0.

Configuration options:

Field Default Description
wayvnc wayvnc-0.10 The wayvnc package to run
openssl openssl Package used to generate credentials
address "localhost" Listen address
port 5900 Listen port
enable-auth? #t Require clients to authenticate
enable-pam? #t Authenticate against PAM instead of a fixed password
username unset User name clients must supply; ignored under PAM
password-file unset File whose first line is the password; ignored under PAM
tls 'generate 'generate, a (key-file . cert-file) pair, or #f for no TLS
tls-common-name "localhost" CN of the generated certificate
tls-subject-alt-names '("DNS:localhost" "IP:127.0.0.1") SANs of the generated certificate
rsa-key 'generate 'generate, a path to your own key, or #f
credentials-directory ".local/state/wayvnc" Credentials and config, relative to $HOME
desktop? #f Capture every output instead of one
output unset Name of the output to capture
seat unset Seat to use for input
desktop-name unset Desktop name reported to clients
max-fps 30 Frame rate limit
gpu? #f Enable the features that need a GPU
render-cursor? #f Draw the cursor into the frame
disable-input? #f Refuse remote input, making the session view-only
detached? #f Start without a compositor and wait to be attached
log-level "warning" error / warning / info / debug / trace / quiet
extra-config '() Extra lines appended to the wayvnc config file
extra-options '() Extra command line options
environment-variables '() NAME=value strings added to wayvnc's environment
auto-start? #f Start with the home Shepherd

Starting it:

auto-start? is #f on purpose: wayvnc needs WAYLAND_DISPLAY pointing at a compositor that's already up, which isn't true at login, and the home Shepherd passes on the environment it inherited back then. Either herd start wayvnc from inside the session, or set detached? #t and attach once it exists:

wayvncctl attach wayland-1   # attach to that compositor's socket
wayvncctl output-list        # show what can be captured
wayvncctl detach

If the socket isn't wayland-0, pin it with (environment-variables '("WAYLAND_DISPLAY=wayland-1")).

Reconfiguring rewrites the config file, but wayvnc can't reload it - a running server keeps the old settings until you restart it.

System Services

Bichon

Bichon is a self-hosted email archiver written in Rust: it syncs IMAP accounts, indexes messages for full-text search (embedded storage — no external database), and serves a REST API and web interface on port 15630. It is not an email client — it cannot send, compose, or reply. The service creates a dedicated bichon:bichon system user, the data directory at /var/lib/bichon, and rotates /var/log/bichon.log — no manual setup beyond the encryption password.

Encryption password: Bichon encrypts stored credentials with a password you provide. The service takes a path to a file holding that password (encrypt-password-file), so the secret never lands in the world-readable store. Create it before first start:

sudo install -d -m 700 /etc/bichon
openssl rand -base64 48 | tr -d '\n' | sudo tee /etc/bichon/encrypt-password >/dev/null
sudo chmod 600 /etc/bichon/encrypt-password

tr -d '\n' strips the trailing newline so the file is exactly the secret. Back it up — this key encrypts your stored IMAP credentials, and it's write-once: regenerating it against an existing data directory makes the stored data undecryptable. Activation warns (without failing) if the file is missing.

Usage:

(use-modules (px services mail))

;; Minimal — encrypt-password-file is required
(service bichon-service-type
         (bichon-configuration
          (encrypt-password-file "/etc/bichon/encrypt-password")))

;; Behind a reverse proxy, with the SMTP receiver enabled
(service bichon-service-type
         (bichon-configuration
          (encrypt-password-file "/etc/bichon/encrypt-password")
          (bind-ip "127.0.0.1")
          (public-url "https://archive.example.org")
          (enable-smtp? #t)))

Default credentials: first start creates admin / admin@bichon — change it immediately via the web interface (Settings → Profile).

Configuration options:

Field Default Description
package bichon The bichon package to use
root-dir "/var/lib/bichon" Data directory (BICHON_ROOT_DIR)
encrypt-password-file (required) Path to a file holding the credential-encryption password (BICHON_ENCRYPT_PASSWORD_FILE)
http-port 15630 HTTP / web interface port (BICHON_HTTP_PORT)
bind-ip unset → 0.0.0.0 Listen address; bichon binds all interfaces when unset (BICHON_BIND_IP)
public-url unset Public URL used in generated links (BICHON_PUBLIC_URL)
base-url unset Base path when behind a reverse proxy (BICHON_BASE_URL)
log-level "info" trace / debug / info / warn / error (BICHON_LOG_LEVEL)
enable-smtp? #f Enable the SMTP receiver (BICHON_ENABLE_SMTP)
smtp-port 2525 SMTP receiver port (BICHON_SMTP_PORT)
index-dir {root}/bichon-indices Full-text index directory (BICHON_INDEX_DIR)
data-dir {root}/bichon-storage Blob storage directory (BICHON_DATA_DIR)
extra-env '() Extra BICHON_* settings as an alist of (string . string)
user "bichon" System user to run as
group "bichon" System group

Any setting not exposed above can be passed through extra-env, e.g. (extra-env '(("BICHON_SYNC_CONCURRENCY" . "8"))).

Required ports:

  • TCP/15630 — HTTP REST API and web interface
  • TCP/2525 — SMTP receiver (only when enable-smtp? is #t)

Storage caveat: Bichon does not support writing data to network filesystems (NFS, CIFS/SMB) — root-dir must be on local storage.

Service management:

herd status bichon   # Check status
herd start bichon    # Start daemon
herd stop bichon     # Stop daemon

Unattended Upgrade

Drop-in replacement for (gnu services admin) unattended-upgrade-service-type with battery awareness. All upstream fields are preserved; additions are skip-on-battery? and system-load-paths.

Usage:

(use-modules (px services unattended-upgrade))

(service unattended-upgrade-service-type
         (unattended-upgrade-configuration
          (schedule "0 17 * * *")
          (skip-on-battery? #t)
          (system-load-paths '("/home/user/dotfiles/system"))
          (channels #~
                    (cons* (channel
                            (name 'my-channel)
                            (url "https://example.com/channel.git"))
                           %default-channels))))

Additional configuration options:

Field Default Description
skip-on-battery? #f Skip upgrade when on battery power
system-load-paths '() Extra -L load paths for guix system reconfigure

All other fields (operating-system-file, schedule, channels, reboot?, services-to-restart, system-expiration, maximum-duration, log-file) match the upstream (gnu services admin) defaults.

Caveat on system-load-paths: Guix only stores the top-level configuration file in the store (/run/current-system/configuration.scm), not its imported modules. If your config imports local modules (e.g. (common)) that live outside a channel, you need system-load-paths so the unattended upgrade can find them. These modules are resolved from disk at upgrade time — not from a stored snapshot — so they should be kept in sync with your configuration.

Battery detection: Reads /sys/class/power_supply/*/type to locate AC adapters and checks their online status via sysfs. Desktops without battery info proceed normally.

Chrony

Runs chronyd, the NTP daemon from the Chrony project. Keeps the system clock in sync with the configured time servers. The service creates a dedicated chrony:chrony system user and the drift directory at /var/lib/chrony — no manual setup required.

Default configuration uses NTS (RFC 8915) to authenticate time packets via TLS, preventing on-path attackers from forging NTP responses. The default sources are a geographically diverse mix of Stratum 1 servers:

server time.cloudflare.com iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
server ptbtime2.ptb.de iburst nts
server ntppool1.time.nl iburst nts
driftfile /var/lib/chrony/drift
ntsdumpdir /var/lib/chrony
makestep 1.0 3
rtcsync

ntsdumpdir caches NTS cookies across restarts so the TLS handshake isn't repeated on every boot. There is currently no NTS pool — TLS certificates break the traditional pool.ntp.org pooling model, so sources are listed individually.

Firewall requirement: NTS needs outbound TCP/4460 (NTS-KE handshake) in addition to the usual UDP/123 (NTP). If TCP/4460 is blocked, chronyc -N authdata will show zeros in the KeyID/Type/KLen columns and the sources will never come up.

First-boot caveat: NTS certificate validation requires a roughly-correct clock. If the RTC is badly wrong, the TLS handshake will fail and chronyd won't be able to bootstrap. On fresh systems with unreliable RTCs, temporarily add an unauthenticated pool 2.pool.ntp.org iburst line until the clock is close enough for TLS to work.

Usage:

(use-modules (px services ntp))

;; Default — five NTS-enabled sources (see above)
(service chrony-service-type)

;; With a custom chrony.conf
(service chrony-service-type
         (chrony-service-configuration
          (config "server time.cloudflare.com iburst nts
server ptbtime1.ptb.de iburst nts
driftfile /var/lib/chrony/drift
ntsdumpdir /var/lib/chrony
makestep 1.0 3
rtcsync
")))

Configuration options:

Field Default Description
package chrony The chrony package to use
config Five NTS sources + driftfile / ntsdumpdir / makestep / rtcsync Raw chrony.conf contents

Service management:

sudo herd status chrony          # Check status
sudo herd configuration chrony   # Print path to generated chrony.conf
sudo chronyc sources             # Show NTP sources and reachability
sudo chronyc tracking            # Show clock sync status
sudo chronyc -N authdata         # Verify NTS: KeyID/Type/KLen should be non-zero

Forseti Stack

Forseti is a login, consent, and account portal for an Ory Kratos (identity) and Ory Hydra (OAuth2/OIDC) deployment. This channel packages all three (forseti, ory-kratos, ory-hydra) and provides a service-type for each, plus a forseti-stack-services helper that wires them together from a single public URL.

The three daemons serve plaintext HTTP on loopback. A TLS-terminating reverse proxy fronts the public origin (base-url) and routes browser traffic to Forseti's public port (3000). Hydra (4444) and Kratos (4433) public ports are reached server-to-server; the admin ports (4445 / 4434) and Forseti's internal listener (8081) must not be exposed.

Postgres uses unix-socket peer authentication: each daemon runs as a system user whose name equals its database role (kratos, hydra, forseti), so no database passwords exist anywhere. The stack creates the roles and databases when manage-postgres? is #t.

Usage (the whole stack):

forseti-stack-services returns a list of services, so splice it into your services field rather than wrapping it in service:

(use-modules (px services forseti))

(operating-system
  ;; ...
  (services
   (append
    (forseti-stack-services
     (forseti-stack-configuration
      (base-url "https://id.example.com")))
    %base-services)))

Secrets: real secrets stay in operator-supplied env-files under secrets-directory (default /etc/forseti, root-owned 0600), sourced by each daemon's start wrapper, never baked into the store. The database DSN is not a secret (peer auth) and is set for you. Create the env-files before first start:

sudo install -d -m 700 /etc/forseti
AUDIT_TOKEN=$(openssl rand -hex 24)   # shared, only needed when audit-webhook? is on

# Hydra: system secret (>= 16 bytes)
printf 'SECRETS_SYSTEM=%s\n' "$(openssl rand -hex 16)" \
  | sudo tee /etc/forseti/hydra.env >/dev/null

# Kratos: cookie (>= 16 bytes) + cipher (EXACTLY 32 bytes)
sudo tee /etc/forseti/kratos.env >/dev/null <<EOF
SECRETS_COOKIE=$(openssl rand -hex 16)
SECRETS_CIPHER=$(openssl rand -hex 16)
FORSETI_AUDIT_TOKEN=$AUDIT_TOKEN
EOF

# Forseti: audit token (must match the kratos one) + cookie secret (>= 32 bytes)
sudo tee /etc/forseti/forseti.env >/dev/null <<EOF
FORSETI_AUDIT__WEBHOOK_TOKEN=$AUDIT_TOKEN
FORSETI_SECURITY__COOKIE_SECRET=$(openssl rand -hex 32)
EOF

sudo chmod 600 /etc/forseti/*.env

openssl rand -hex 16 produces 32 hex characters (32 bytes), which satisfies the Kratos cipher length. When audit-webhook? is #f, the FORSETI_AUDIT_TOKEN line is unnecessary.

Reverse proxy (HAProxy, single host): the three daemons share one public origin (base-url), split by path. Forseti is at the root; Hydra mounts under /hydra and Kratos under /kratos, and the proxy strips those prefixes before the upstream (Hydra and Kratos do not honour subpath mounting). The generated configs follow this layout: Hydra's issuer/public is base-url/hydra, Kratos's base_url is base-url/kratos, and Forseti's login/consent pages plus the DCR endpoint stay at the root. This matches the "Shape 1" topology in Forseti's own docs/operator-guide-proxy.md, which is the authoritative reference (it also covers subdomain and CORS/cookie details).

HAProxy is the recommended proxy. The config below targets the stack's loopback ports:

frontend fe_accounts
    bind *:443 ssl crt /etc/haproxy/certs/accounts.example.com.pem alpn h2,http/1.1
    http-request redirect scheme https code 301 unless { ssl_fc }

    # Trust only our own forwarded headers (strip client-sent first).
    http-request del-header X-Forwarded-For
    http-request del-header X-Forwarded-Proto
    http-request del-header X-Forwarded-Host
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Forwarded-Host  %[req.hdr(host)]
    http-request set-header X-Real-IP         %[src]
    http-request set-header X-Forwarded-For   %[src]

    # Capture the routing target in a var BEFORE replace-path mutates the path:
    # use_backend ACLs are evaluated after the rewrite, so a path_beg test on
    # the use_backend line would miss once the prefix has been stripped.
    acl p_hydra  path_beg /hydra/
    acl p_kratos path_beg /kratos/
    http-request set-var(txn.be) str(hydra)  if p_hydra
    http-request set-var(txn.be) str(kratos) if p_kratos

    # Strip the prefix before the upstream sees it (they serve at root).
    http-request replace-path ^/hydra/?(.*)  /\1 if p_hydra
    http-request replace-path ^/kratos/?(.*) /\1 if p_kratos

    use_backend be_hydra  if { var(txn.be) -m str hydra }
    use_backend be_kratos if { var(txn.be) -m str kratos }
    default_backend be_forseti

backend be_forseti
    server forseti 127.0.0.1:3000 check
backend be_hydra      # Hydra public; admin :4445 stays on loopback, never proxied
    server hydra  127.0.0.1:4444 check
backend be_kratos     # Kratos public; admin :4434 stays on loopback, never proxied
    server kratos 127.0.0.1:4433 check

X-Forwarded-Proto: https is mandatory: without it Hydra and Kratos emit http:// URLs and drop Secure from cookies. The generated Hydra config trusts that header from loopback (serve.tls.allow_termination_from), so no --dev is needed in production. Never proxy the admin ports (4445 / 4434) or Forseti's internal listener (8081). If you add a Content-Security-Policy, it must allow Forseti's inline pre-paint theme script (see the operator guide).

Quick test without a proxy: set dev? to #t so the Ory daemons accept the https issuer over plain HTTP. Never use dev? in production.

forseti-stack-configuration options:

Field Default Description
base-url (required) Public https origin; the issuer and all UI/redirect URLs derive from it
manage-postgres? #t Provision Postgres plus the kratos/hydra/forseti roles and databases
secrets-directory "/etc/forseti" Directory holding the per-service env-files
smtp-connection-uri unset Kratos courier SMTP URI for verification/recovery mail
enable-dynamic-client-registration? #f Enable Hydra DCR (/oauth2/register) for MCP-style clients
audit-webhook? #t Emit the Kratos audit web_hook to Forseti's internal listener
dev? #f Run the Ory daemons with --dev (no proxy needed); never in production
kratos / hydra / forseti unset Override the generated config file for a service with your own file-like
postgres-host "localhost" Reserved; peer auth uses the local socket
kratos-log-file / hydra-log-file / forseti-log-file /var/log/{kratos,hydra,forseti}.log Per-service log files

Standalone services: Hydra and Kratos do not require Forseti. Use ory-hydra-service-type / ory-kratos-service-type directly with your own config-file; each runs a one-shot migrate sql (gated by auto-migrate?) before the serve daemon.

Field (ory-kratos / ory-hydra) Default Description
config-file (required) The kratos.yml / hydra.yml
environment-variables '() Non-secret env (e.g. DSN) as a (string . string) alist
environment-file /etc/forseti/{kratos,hydra}.env Operator-supplied secrets file, sourced at start
auto-migrate? #t Run migrate sql as a one-shot before serve
dev? #f Append --dev to the serve command
user / group "ory" System user/group (the stack overrides these to the per-role names)
state-directory /var/lib/ory-{kratos,hydra} Daemon working directory
log-file /var/log/{kratos,hydra}.log Shepherd log file
requirement '(postgres) Extra shepherd requirements

forseti-service-type mirrors these (default user/group forseti, requirement '(hydra kratos postgres), no auto-migrate? / dev?). It reads its config via FORSETI_CONFIG_PATH and uses sqlite under its state directory unless you set FORSETI_DATABASE__URL in forseti.env.

Service management:

herd status                  # list all services
herd status forseti          # one daemon
sudo herd start postgres-roles   # one-shot: create roles + databases

# Verify the stack is wired
curl -s http://localhost:4444/.well-known/openid-configuration   # issuer == base-url
curl -s http://localhost:3000/healthz                            # forseti -> ok

IOTA Node

Run an IOTA full node or validator node.

Prerequisites:

  1. Create configuration file (e.g., /etc/iota/fullnode.yaml)
  2. Download genesis blob for your network (mainnet/testnet/devnet)
  3. For validators: generate key pairs

Usage:

(use-modules (px services iota))

;; Basic full node
(service iota-node-service-type
         (iota-node-configuration
          (config-file "/etc/iota/fullnode.yaml")))

;; With custom settings
(service iota-node-service-type
         (iota-node-configuration
          (config-file "/etc/iota/validator.yaml")
          (data-directory "/var/lib/iota")
          (log-file "/var/log/iota-node.log")
          (log-level "info,iota_core=debug,consensus=debug")))

Service management:

herd status iota-node   # Check status
herd start iota-node    # Start node
herd stop iota-node     # Stop node

Configuration options:

Field Default Description
config-file (required) Path to fullnode.yaml or validator.yaml
user "iota" System user to run as
group "iota" System group
data-directory "/var/lib/iota" Database and state storage
log-file "/var/log/iota-node.log" Log output location
log-level "info,iota_core=debug,..." Rust log levels

Required ports:

  • TCP/9000 - JSON-RPC
  • UDP/8084 - P2P sync

RealtimeKit

RealtimeKit grants real-time scheduling to user processes on request. Required by PipeWire and PulseAudio for low-latency audio.

Usage:

(use-modules (px services audio))

(service rtkit-daemon-service-type)

Tailscale

Tailscale is a zero-config VPN built on WireGuard.

Usage:

(use-modules (px services networking))

(service tailscale-service-type)

After reconfiguration:

tailscale up         # Authenticate and connect
tailscale status     # Check connection status

vpnmux

Runs the vpnmux daemon, a control loop that keeps Mullvad and Tailscale from conflicting at the netfilter and DNS layer. It continuously reconciles the system to a desired provider state by driving nft, mullvad, and tailscale. The service wires those binaries via VPNMUX_NFT/VPNMUX_MULLVAD/VPNMUX_TAILSCALE, creates /run/vpnmux and /var/lib/vpnmux, and installs the vpnmux CLI into the system profile.

By default the activation creates a vpnmux system group and chowns the runtime dirs to root:vpnmux with mode 02770 (setgid, group rwx), so members of the group can drive vpnmux set/status without sudo. Set group to "" to keep the dirs root-only.

Usage:

(use-modules (px services networking))

(service vpnmux-service-type)

;; Verbose logging in the shepherd log
(service vpnmux-service-type
         (vpnmux-configuration
          (log-level "debug")))

;; Root-only CLI (sudo required for set/status)
(service vpnmux-service-type
         (vpnmux-configuration
          (group "")))

Add yourself to the vpnmux group to use the CLI without sudo.

Configuration options:

Field Default Description
vpnmux vpnmux The vpnmux package to run
nftables nftables Package providing nft (VPNMUX_NFT)
mullvad mullvad-vpn-desktop Package providing the mullvad CLI (VPNMUX_MULLVAD)
tailscale tailscale Package providing the tailscale CLI (VPNMUX_TAILSCALE)
log-level unset When set, exported as VPNMUX_LOG (e.g. "debug")
group "vpnmux" System group permitted to drive the CLI without sudo. Empty string keeps dirs root-only. Exported as VPNMUX_GROUP.

After reconfiguration:

vpnmux status                 # Show current state (no sudo if in vpnmux group)
vpnmux set mullvad            # Desired: Mullvad only
vpnmux set tailscale          # Desired: Tailscale only
vpnmux set mullvad tailscale  # Both, coexisting
vpnmux set                    # Neither (empty set)
herd status vpnmux            # Daemon status

AppArmor

Mounts securityfs and loads AppArmor profiles into the kernel with apparmor_parser, at boot and again on every reconfigure. Profiles are file-like objects and load straight from the store, so a profile change is a reconfigure away and never needs a copy under /etc. The service has no opinion about what the profiles confine.

The kernel must have AppArmor in its LSM stack — check with cat /sys/module/apparmor/parameters/enabled (Y) and add apparmor to the lsm= kernel argument if it isn't there.

The service defaults to px's apparmor 5.0.2 rather than the 4.1.2 in Guix, because 4.1.2 cannot emit network_v9 policy. That matters for any profile that denies a unix socket by path: under 4.1.2 the rule blocks open() but not connect(), so the socket stays reachable and the rule looks like it works. Declare abi <abi/5.0>, in such a profile — 5.0.2 ships that feature file, and 4.1.2 fails loudly on it rather than compiling something weaker.

Usage:

(use-modules (px services apparmor))

(service apparmor-service-type
         (apparmor-configuration
          (profiles (list (local-file "my-app.profile")))))

A profile attaches by executable path, which on Guix changes with every upgrade, so glob the store path:

abi <abi/5.0>,
include <tunables/global>

profile my-app /gnu/store/*-my-app-*/bin/my-app {
  include <abstractions/base>
  file,
  audit deny /run/dbus/system_bus_socket rw,
}

deny rules are not enforced in complain mode, whatever aa-complain(8) says — a profile carrying flags=(complain) denies nothing at all. Complain mode only logs what the allow rules missed, so a profile whose point is its denies has to run in enforce.

Check a profile before reconfiguring — -b points its include directives at the package's own policy:

P=$(guix build -L ~/git/panther -e '(@ (px packages apparmor) apparmor)')
$P/sbin/apparmor_parser -Q --config-file=/dev/null -b $P/etc/apparmor.d my-app.profile

Configuration options:

Field Default Description
package apparmor (px, 5.0.2) The apparmor package providing the parser and tools
profiles '() List of file-like objects, each an AppArmor profile to load

After reconfiguration:

sudo aa-status                                  # Which profiles are loaded, and in which mode
sudo herd status apparmor                       # The parser one-shot; stopped is its normal state
sudo herd status file-system-/sys/kernel/security

The aa-* tools and apparmor_parser live in sbin, not bin. Mode is set declaratively in the profile text, not with aa-complain/aa-enforce — store profiles are immutable, and the next reconfigure would undo the change anyway.

USBGuard

Runs usbguard-daemon to enforce a USB device authorization policy — a whitelist for USB devices that blocks BadUSB-style attacks. The generated usbguard-daemon.conf lives in the store; rules are kept at /etc/usbguard/rules.conf so they can be updated without a reconfigure.

Usage:

(use-modules (px services usbguard))

;; Default: block everything not explicitly allowed, only root can use IPC
(service usbguard-service-type)

;; Allow members of the 'usbguard' group to manage rules via the CLI
(service usbguard-service-type
         (usbguard-configuration
          (ipc-allowed-groups '("usbguard"))))

Add yourself to the usbguard group (created by the service) to use the CLI without sudo.

Before first start:

With implicit-policy-target set to 'block (the default), the daemon will block everything that isn't explicitly allowed — including devices already plugged in when it starts. Generate an initial policy from your currently-connected devices so you don't lock yourself out of your own keyboard:

sudo sh -c 'usbguard generate-policy > /etc/usbguard/rules.conf'
sudo herd restart usbguard

Run this once, with only the devices you trust plugged in. Review /etc/usbguard/rules.conf afterwards and trim anything you don't want whitelisted.

Managing rules without reconfiguring:

# Via the CLI — daemon persists changes to /etc/usbguard/rules.conf
sudo usbguard list-devices
sudo usbguard allow-device <id> -p       # -p = permanent
sudo usbguard append-rule 'allow id 1d6b:0002'

# Or edit the file directly
sudo $EDITOR /etc/usbguard/rules.conf
sudo herd restart usbguard

Changing fields in usbguard-configuration (policy targets, IPC allow-lists, audit settings) does require guix system reconfigure — those are baked into the store config.

Configuration options:

Field Default Description
package usbguard The usbguard package to use
rule-file "/etc/usbguard/rules.conf" Persistent rules file
rule-folder "/etc/usbguard/rules.d/" Directory of additional rule files
implicit-policy-target 'block Action for devices not matching any rule ('allow, 'block, 'reject)
present-device-policy 'apply-policy How to treat devices already connected at daemon start
present-controller-policy 'keep Same, for USB controllers
inserted-device-policy 'apply-policy How to treat newly-inserted devices
authorized-default 'none Default authorization for new devices ('none, 'all, 'keep, 'internal)
device-manager-backend 'uevent Backend ('uevent or 'umockdev)
ipc-allowed-users '("root") Users permitted to use the IPC interface
ipc-allowed-groups '() Groups permitted to use the IPC interface
audit-backend 'FileAudit 'FileAudit or 'LinuxAudit
audit-file-path "/var/log/usbguard/usbguard-audit.log" Audit log location
hide-pii? #f Strip serial numbers and descriptor hashes from audit entries
log-file "/var/log/usbguard.log" Shepherd log file
auto-start? #t Start the daemon automatically at boot

Service management:

herd status usbguard    # Check status
herd restart usbguard   # Reload after editing rules.conf by hand

Hardening: The daemon is launched with -C (drop capabilities after startup) and -W (seccomp syscall allowlist). The D-Bus configuration and Polkit action from the usbguard package are registered automatically, so usbguard-dbus and desktop front-ends work without extra wiring.

wayvnc PAM

Installs /etc/pam.d/wayvnc, the policy the wayvnc home service authenticates against when enable-pam? is on. wayvnc calls pam_start with that service name but ships no policy file, so without this the login falls through to Guix's deny-all other - closed, but useless.

users is mandatory and it's the reason the service exists: wayvnc passes the client-supplied user name straight to PAM without comparing it to anything, so on a stock policy any account on the machine gets in - and whoever gets in drives the session being shared, no matter whose password they used. The generated policy puts pam_succeed_if in front of pam_unix to stop that. Listing several accounts gives each of them that power over the others, so prefer a single name.

Usage:

(use-modules (px services vnc))

(service wayvnc-pam-service-type
         (wayvnc-pam-configuration
          (users '("franz"))))

;; Tighter lockout
(service wayvnc-pam-service-type
         (wayvnc-pam-configuration
          (users '("franz"))
          (deny 3)
          (fail-interval 300)
          (unlock-time 1800)))

Configuration options:

Field Default Description
users (required) Accounts allowed to authenticate against wayvnc
deny 3 Consecutive failures before the account is locked out
fail-interval 900 Window, in seconds, over which failures are counted
unlock-time 600 Seconds before a locked account may try again
faillock-directory "/var/lib/wayvnc/faillock" Directory holding the per-user failure tallies

Two details in the generated policy are deliberate. The tally directory is /var/lib/wayvnc/faillock, not pam_faillock's /var/run/faillock default: nothing on Guix creates that, and wayvnc runs unprivileged so it can't either, at which point the module reads the missing tally as "no failures yet" and lockout silently never happens. Activation creates it root-owned with one tally per listed account, owned user:root at 0660 - a directory the users could write to would let them plant tallies for each other.

Every module gets nodelay, because wayvnc calls PAM synchronously from its event loop and the two second failure delay would freeze capture and input for everyone connected. libpam applies the largest delay any module asks for, so it has to be off everywhere - upstream's own wayvnc.pam misses that and ends up with neither lockout nor a delay. Measured: ~2.1s for a rejection either way, against 3.5-4.3s with the delay left in.

Inspecting lockouts:

sudo faillock --dir /var/lib/wayvnc/faillock --user franz           # recorded failures
sudo faillock --dir /var/lib/wayvnc/faillock --user franz --reset   # clear them
sudo cat /etc/pam.d/wayvnc                                          # the generated policy

NSS FHS

Symlinks the NSS (Network Security Services) libraries into /usr/lib/nss. Applications that dlopen NSS instead of linking against it usually hard-code the FHS paths and find nothing on Guix. Guix does the same thing for Dovecot, which hard-codes /usr/lib/dovecot.

The one that needs it here is autofirma: it walks a fixed list of directories looking for libsoftokn3.so, and without a hit it cannot read certificates out of the Firefox or Chromium key stores - signing then only works from a PKCS#12 file or a smart card. /usr/lib/nss is the first entry on that list a Guix system can populate.

The lookup wants libsoftokn3.so, the NSPR libraries and libsqlite3.so in one directory, so the service builds a union of the three packages rather than symlinking nss alone.

Usage:

(use-modules (px services nss))

(service nss-fhs-service-type)
Field Default Meaning
nss nss Package providing lib/nss
nspr nspr Package providing the NSPR libraries
sqlite sqlite Package providing libsqlite3.so

Verifying:

ls -l /usr/lib/nss                       # -> /gnu/store/...-nss-fhs
autofirmacl listaliases -store mozilla   # lists your Firefox/Chromium certificates

System Configuration

This channel provides pre-configured building blocks for Guix system definitions. Import with:

(use-modules (px system os))

Packages

Variable Description
%os-base-packages Extends %base-packages with wpa-supplicant, libimobiledevice, neovim
%os-desktop-packages Extends %os-base-packages with guix-gui (graphical Guix frontend)

Services

Variable Description
%os-base-services Extends %base-services with panther channel and substitute servers
%os-desktop-services Extends %desktop-services with panther channel and substitute servers
%os-desktop-services-minimal Desktop services without login/display managers and audio (for custom greeter setups)

Operating Systems

Variable Description
%os-base Minimal OS with %os-base-services and %os-base-packages

When to Use What

  • Headless server: Use %os-base directly or inherit from it
  • Desktop with GDM/SDDM: Inherit %os-base, use %os-desktop-services, and set %os-desktop-packages
  • Desktop with custom greeter (greetd, etc.): Use %os-desktop-services-minimal to avoid conflicts

Usage

Inherit from an OS definition and customize:

(operating-system
  (inherit %os-base)
  (host-name "my-workstation")
  (timezone "Europe/Berlin")
  ;; Add your file-systems, users, etc.
  (services
   (cons* (service openssh-service-type)
          %os-base-services)))

Or use just the services/packages in your own OS:

(operating-system
  ;; ...your configuration...
  (packages
   (cons* my-extra-package
          %os-base-packages))
  (services
   (modify-services %os-desktop-services-minimal
     (elogind-service-type config =>
       (elogind-configuration
         (inherit config)
         (handle-lid-switch 'suspend))))))

Time Travel

When things break because of upstream changes, this will allow you to run a future guix commit, to fix and test the channel without updating the whole system.

Create a channels file that includes only the guix channel:

(list (channel
        (name 'guix)
        (url "https://codeberg.org/guix/guix.git")
        (branch "master")
        ;; Specify commit
        (commit "dc1a77267f03e37b331c8597b066c5ee52a75445")
        (introduction
          (make-channel-introduction
            "9edb3f66fd807b096b48283debdcddccfea34bad"
            (openpgp-fingerprint
              "BBB0 2DDF 2CEA F6A8 0D1D  E643 A2A0 6DF2 A33A 54FA")))))

Spawn a shell for a clean environment:

guix shell --container --nesting --network openssl nss-certs coreutils guix

Known Issues

  • Channel modules shadowed by system profile (bug #74396): After guix pull, new package versions may not be available until you also run guix system reconfigure. Workaround: keep system and user guix in sync, or use guix time-machine -C ~/.config/guix/channels.scm -- shell <package>.

Upstream

Packages here may be upstreamed to Guix if they meet Guix's free software requirements. Please include me in the copyright notice if you do.