diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,16 @@
+propellor (5.18) unstable; urgency=medium
+
+  * Git.daemonRunning: Worked around git's unfortunate new
+    requirement that safe.directory be set when running git-daemon from
+    inetd.
+  * Git.daemonService: New property which runs git-daemon as a systemd
+    service.
+  * Systemd.nspawned: Fix a bug that prevented propellor from running in
+    a systemd machine due to /usr/local/propellor not being visible
+    inside the machine.
+
+ -- Joey Hess <id@joeyh.name>  Sun, 10 Aug 2025 11:24:34 -0400
+
 propellor (5.17) unstable; urgency=medium
 
   * Apt: Enable the non-free-firmware section, when used with Debian
diff --git a/config-freebsd.hs b/config-freebsd.hs
--- a/config-freebsd.hs
+++ b/config-freebsd.hs
@@ -59,7 +59,7 @@
 -- A generic webserver in a Docker container.
 webserverContainer :: Docker.Container
 webserverContainer = Docker.container "webserver" (Docker.latestImage "debian") $ props
-	& osDebian' KFreeBSD (Stable "bookworm") X86_64
+	& osDebian' KFreeBSD (Stable "trixie") X86_64
 	& Apt.stdSourcesList
 	& Docker.publish "80:80"
 	& Docker.volume "/var/www:/var/www"
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
+propellor (5.18) unstable; urgency=medium
+
+  * Git.daemonRunning: Worked around git's unfortunate new
+    requirement that safe.directory be set when running git-daemon from
+    inetd.
+  * Git.daemonService: New property which runs git-daemon as a systemd
+    service.
+  * Systemd.nspawned: Fix a bug that prevented propellor from running in
+    a systemd machine due to /usr/local/propellor not being visible
+    inside the machine.
+
+ -- Joey Hess <id@joeyh.name>  Sun, 10 Aug 2025 11:24:34 -0400
+
 propellor (5.17) unstable; urgency=medium
 
   * Apt: Enable the non-free-firmware section, when used with Debian
diff --git a/joeyconfig.hs b/joeyconfig.hs
--- a/joeyconfig.hs
+++ b/joeyconfig.hs
@@ -31,7 +31,6 @@
 import qualified Propellor.Property.FlashKernel as FlashKernel
 import qualified Propellor.Property.Laptop as Laptop
 import qualified Propellor.Property.LightDM as LightDM
-import qualified Propellor.Property.Debootstrap as Debootstrap
 import qualified Propellor.Property.HostingProvider.Linode as Linode
 import qualified Propellor.Property.SiteSpecific.GitHome as GitHome
 import qualified Propellor.Property.SiteSpecific.GitAnnexBuilder as GitAnnexBuilder
@@ -48,7 +47,7 @@
 	, dragon
 	, oyster
 	, house
-	, sky
+	, gate
 	, kite
 	, sparrow
 	, beaver
@@ -62,47 +61,43 @@
 darkstar :: Host
 darkstar = host "darkstar.kitenet.net" $ props
 	& osDebian Unstable X86_64
-	& ipv6 "2001:4830:1600:187::2"
 	& Hostname.sane
 	& Hostname.mailname
 	& Apt.serviceInstalledRunning "swapspace"
 	& Laptop.powertopAutoTuneOnBoot
 	& Laptop.trimSSD
-	& Grub.cmdline_Linux_default "i915.enable_psr=1"
 	! Grub.cmdline_Linux_default "quiet"
 	& User.hasGroup (User "joey") (Group "dialout")
 
 	& JoeySites.dkimMilter
 	& JoeySites.postfixSaslPasswordClient
-	-- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
-	--	"/usr/bin/timeout 45m /home/joey/bin/goodmorning"
 	& JoeySites.laptopSoftware
 	& JoeySites.userDirHtml
+	& JoeySites.thinkPadLedsOff
 	& Ssh.userKeys (User "joey") hostContext
 		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfFntnesZcYz2B2T41ay45igfckXRSh5uVffkuCQkLv joey@darkstar")
 		]
-	& imageBuiltFor house
+	! imageBuiltFor house
 		(RawDiskImage "/srv/house.img")
 		(Debootstrapped mempty)
 
 dragon :: Host
 dragon = host "dragon.kitenet.net" $ props
-	& ipv6 "2001:4830:1600:187::2"
 	& JoeySites.dkimMilter
 	& JoeySites.postfixSaslPasswordClient
 
 oyster :: Host
 oyster = host "oyster.kitenet.net" $ props
-	& standardSystem (Stable "buster") X86_64
+	& standardSystem (Stable "bookworm") X86_64
 		["Unreliable server. Anything here may be lost at any time!" ]
-	& ipv4 "45.138.157.89"
+	& ipv4 "45.87.155.118"
 
 	& User.hasPassword (User "root")
 	& Apt.unattendedUpgrades
 
 house :: Host
 house = host "house.lan" $ props
-	& standardSystem Testing ARMHF [ "House mail computer." ]
+	& standardSystem (Stable "bookworm") ARMHF [ "House main computer." ]
 	& Apt.removed ["rsyslog"]
 	
 	& cubietech_Cubietruck
@@ -133,53 +128,69 @@
 		(Context "house.joeyh.name")
 		(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMAmVYddg/RgCbIj+cLcEiddeFXaYFnbEJ3uGj9G/EyV joey@honeybee")
 	
-	-- Use iwd to connect to wifi on whatever usb wifi is connected.
-	& Apt.installed ["iwd"]
-	& File.hasContent "/var/lib/iwd/.known_network.freq"
-		[ "[7085c93b-9e5f-5cdb-a13d-bf72db7c3adf]"
-		, "name=/var/lib/iwd//hollow.open"
-		, "list= 2457"
-		]
-	& File.hasContent "/var/lib/iwd/hollow.open"
-		[ "[IPv4]"
-		, "Address=10.1.1.2"
-		, "Netmask=255.255.255.0"
-		, "Gateway=10.1.1.1"
-		, "Broadcast=10.1.1.255"
-		, "DNS=10.1.1.1"
+	-- Use network-manager to connect to wifi on whatever usb wifi is connected.
+	& Apt.installed ["network-manager"]
+	& File.hasContent "/etc/NetworkManager/system-connections/hollow.nmconnection"
+		[ "[connection]"
+		, "id=hollow"
+		, "uuid=dfa672be-79d2-4151-9a26-ae7cb3729681"
+		, "type=wifi"
+		, "interface-name=wlan1"
+		, ""
+		, "[wifi]"
+		, "mode=infrastructure"
+		, "ssid=hollow"
+		, ""
+		, "[ipv4]"
+		, "method=manual"
+		, "addresses=10.1.1.2/24"
+		, "gateway=10.1.1.1"
+		, "dns=10.1.1.1"
+		, ""
+		, "[ipv6]"
+		, "addr-gen-mode=default"
+		, "method=auto"
+		, ""
+		, "[proxy]"
 		]
-	& Systemd.enabled "iwd"
+	& Systemd.enabled "NetworkManager"
 	& "/etc/resolv.conf" `File.containsLine` "search lan"
 
-	& JoeySites.homeNAS
 	& Apt.installed ["mtr-tiny", "iftop", "screen", "nmap"]
-	-- Currently manually building the xr_usb_serial module.
-	& Apt.installed ["linux-headers-armmp-lpae"]
 	& Postfix.satellite
 
-sky :: Host
-sky = host "sky.lan" $ props
-	& standardSystem Testing ARMHF [ "Wifi router." ]
+-- This is a raspberry pi and is installed using https://raspi.debian.net/tested-images/
+-- before propellor is run on it.
+gate :: Host
+gate = host "gate.lan" $ props
+	& standardSystem (Stable "trixie") ARM64 [ "Wifi router." ]
 	& Apt.removed ["rsyslog"]
 	
 	& Apt.installed ["raspi-firmware", "firmware-misc-nonfree"]
 	& Apt.serviceInstalledRunning "systemd-timesyncd" -- no hardware clock
 
+	& "/etc/systemd/network/10-gate.link" `File.containsLines`
+		[ "[Match]"
+		, "MACAddress=9c:ef:d5:fc:d6:f3" -- USB wifi adapter
+		, ""
+		, "[Link]"
+		, "Name=" ++ JoeySites.wifiInterface ifs
+		]
+
 	& Ssh.hostKeys hostContext
 		[ (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBG19v7K59SzFp/OntM9iyhlKOj2pShFSPJeqR1aiYhPF2NqztcmsY6WvIDqh6jmaISnyV1IqZZ60zvGTVRoOyMY=")
 		]
 	& Ssh.userKeys (User "joey") hostContext
 		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOmwEfM5qTjA7xuJgygEgHfa1Y/WrRBpu7xBY8D82ul+")
 		]
+	& Postfix.satellite
 
 	& JoeySites.connectStarlinkDish ifs
-	& JoeySites.homeRouter ifs "hollow" JoeySites.hostapd2GhzConfig_mt76
-	& Apt.installed ["mtr-tiny", "iftop", "screen", "nmap", "net-tools"]
-	& Postfix.satellite
+	& JoeySites.homeRouter "gate.lan" ifs "hollow" JoeySites.hostapd2GhzConfig_mt76
   where
 	ifs = JoeySites.Interfaces
 		{ JoeySites.ethernetInterface = "eth0"
-		, JoeySites.wifiInterface = "wlan0"
+		, JoeySites.wifiInterface = "wlan-gate"
 		, JoeySites.wifiInterfaceOld = "wlan1"
 		}
 
@@ -188,12 +199,9 @@
 -- with propellor.
 kite :: Host
 kite = host "kite.kitenet.net" $ props
-	& standardSystemUnhardened Testing X86_64 [ "Welcome to kite!" ]
+	& standardSystemUnhardened (Stable "trixie") X86_64 [ "Welcome to kite!" ]
 	& ipv4 "66.228.36.95"
-	-- disabled because I've had complaints of mail bouncing, and it
-	-- seems mail servers are only trying ipv6 and then bouncing if
-	-- they have an ipv6 routing issue.
-	-- & ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
+	& ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
 	& alias "kitenet.net"
 	& Ssh.hostKeys (Context "kitenet.net")
 		[ (SshDsa, "ssh-dss AAAAB3NzaC1kc3MAAACBAO9tnPUT4p+9z7K6/OYuiBNHaij4Nzv5YVBih1vMl+ALz0gYAj8RWJzXmqp5buFAyfgOoLw+H9s1bBS01Sy3i07Dm6cx1fWG4RXL/E/3w1tavX99GD2bBxDBu890ebA5Tp+eFRJkS9+JwSvFiF6CP7NbVjifCagoUO56Ig048RwDAAAAFQDPY2xM3q6KwsVQliel23nrd0rV2QAAAIEAga3hj1hL00rYPNnAUzT8GAaSP62S4W68lusErH+KPbsMwFBFY/Ib1FVf8k6Zn6dZLh/HH/RtJi0JwdzPI1IFW+lwVbKfwBvhQ1lw9cH2rs1UIVgi7Wxdgfy8gEWxf+QIqn62wG+Ulf/HkWGvTrRpoJqlYRNS/gnOWj9Z/4s99koAAACBAM/uJIo2I0nK15wXiTYs/NYUZA7wcErugFn70TRbSgduIFH6U/CQa3rgHJw9DCPCQJLq7pwCnFH7too/qaK+czDk04PsgqV0+Jc7957gU5miPg50d60eJMctHV4eQ1FpwmGGfXxRBR9k2ZvikWYatYir3L6/x1ir7M0bA9IzNU45")
@@ -214,6 +222,7 @@
 	& Fail2Ban.installed -- since ssh password authentication is allowed
 	-- Allow ssh -R to forward ports via kite
 	& Ssh.setSshdConfig "GatewayPorts" "clientspecified"
+	& Apt.installed ["wtmpdb"]
 	& Apt.serviceInstalledRunning "ntp"
 	& "/etc/timezone" `File.hasContent` ["US/Eastern"]
 	
@@ -274,9 +283,6 @@
 	& alias "kgb.kitenet.net"
 	& JoeySites.kgbServer
 	
-	& alias "podcatcher.kitenet.net"
-	& JoeySites.podcatcher
-
 	& alias "ns4.kitenet.net"
 	& myDnsPrimary "kitenet.net"
 		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
@@ -313,11 +319,14 @@
 	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
 		(GitAnnexBuilder.standardAutoBuilder True)
 		Unstable X86_64 mempty Nothing (Cron.Times "15 * * * *") "2h")
+	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
+		(GitAnnexBuilder.standardAutoBuilder True)
+		Unstable X86_32 mempty Nothing (Cron.Times "30 * * * *") "2h")
 	& Apt.serviceInstalledRunning "swapspace"
 
 sparrow :: Host
 sparrow = host "sparrow.kitenet.net" $ props
-	& standardSystem Testing ARM64 [ "Welcome to sparrow!" ]
+	& standardSystem (Stable "trixie") ARM64 [ "Welcome to sparrow!" ]
 	& ipv4 "128.140.52.168"
 	& ipv6 "2a01:4f8:c17:ed3a::1"
 	& Apt.installed ["ssh"]
@@ -326,16 +335,6 @@
 	& Apt.serviceInstalledRunning "swapspace"
 
 	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
-		-- qemu hangs running unattended-upgrades on i386
-		-- so disable installing that
-		(GitAnnexBuilder.standardAutoBuilder False)
-		Unstable X86_32 mempty Nothing (Cron.Times "30 * * * *") "4h")
-	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
-		GitAnnexBuilder.stackAutoBuilder
-		(Stable "jessie") X86_32 
-		(Debootstrap.UseOldGpgKeyring Debootstrap.:+ Debootstrap.DebootstrapMirror "http://archive.debian.org/debian/")
-		(Just "ancient") (Cron.Times "45 * * * *") "4h")
-	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
 		(GitAnnexBuilder.standardAutoBuilder True)
 		Testing ARM64 mempty Nothing (Cron.Times "1 * * * *") "2h")
 	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
@@ -344,9 +343,21 @@
 		(Just "ancient") (Cron.Times "20 * * * *") "2h")
 	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
 		(GitAnnexBuilder.armAutoBuilder (GitAnnexBuilder.standardAutoBuilder True))
-		Testing ARMEL mempty Nothing (Cron.Times "15 15 * * *") "2h")
+		Testing ARMEL mempty Nothing (Cron.Times "40 * * * *") "2h")
 	
-	& Systemd.nspawned ancientKitenet
+	! Systemd.nspawned ancientKitenet
+	
+	& Borg.backup "/root" (JoeySites.rsyncNetBorgRepo "sparrow.borg" []) Cron.Daily
+		[ ]
+		[ Borg.KeepDays 7
+		, Borg.KeepWeeks 4
+		, Borg.KeepMonths 3
+		]
+		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
+		`requires` Ssh.userKeys (User "root")
+			(Context "sparrow.kitenet.net")
+			[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID3mgBXLLzp+DnzZqH6Azly+0uZe/d2TK69oZYcFz+gN root@sparrow")
+			]
 
 beaver :: Host
 beaver = host "beaver.kitenet.net" $ props
@@ -364,7 +375,6 @@
 
 mouse :: Host
 mouse = host "mouse.kitenet.net" $ props
-	& ipv4 "67.223.19.96"
 	& Apt.installed ["ssh"]
 	& Tor.installed
 	& Tor.hiddenServiceAvailable "ssh" (Port 22)
@@ -403,7 +413,7 @@
 	& alias "dist-bugs.kitenet.net"
 	& alias "family.kitenet.net"
 
-	& osDebian (Stable "buster") X86_32
+	& osDebian (Stable "bookworm") X86_32
 	& Apt.installed ["linux-image-686-pae"]
 	& Apt.stdSourcesList `onChange` Apt.upgrade
 	& Apt.unattendedUpgrades
@@ -423,7 +433,7 @@
 -- Exhibit: kite's 90's website on port 1994.
 ancientKitenet :: Systemd.Container
 ancientKitenet = Systemd.debContainer "ancient-kitenet" $ props
-	& standardContainer (Stable "buster") ARM64
+	& standardContainer (Stable "trixie") ARM64
 	& alias hn
 	& Git.cloned (User "root") "git://kitenet-net.branchable.com/" "/var/www/html"
 		(Just "remotes/origin/old-kitenet.net")
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 5.17
+Version: 5.18
 Cabal-Version: 1.20
 License: BSD2
 Maintainer: Joey Hess <id@joeyh.name>
diff --git a/src/Propellor/Info.hs b/src/Propellor/Info.hs
--- a/src/Propellor/Info.hs
+++ b/src/Propellor/Info.hs
@@ -91,13 +91,13 @@
 -- It also lets the type checker know that all the properties of the
 -- host must support Debian.
 --
--- >	& osDebian (Stable "bookworm") X86_64
+-- >	& osDebian (Stable "trixie") X86_64
 osDebian :: DebianSuite -> Architecture -> Property (HasInfo + Debian)
 osDebian = osDebian' Linux
 
 -- Use to specify a different `DebianKernel` than the default `Linux`
 --
--- >	& osDebian' KFreeBSD (Stable "bookworm") X86_64
+-- >	& osDebian' KFreeBSD (Stable "trixie") X86_64
 osDebian' :: DebianKernel -> DebianSuite -> Architecture -> Property (HasInfo + Debian)
 osDebian' kernel suite arch = tightenTargets $ os (System (Debian kernel suite) arch)
 
diff --git a/src/Propellor/Property/Git.hs b/src/Propellor/Property/Git.hs
--- a/src/Propellor/Property/Git.hs
+++ b/src/Propellor/Property/Git.hs
@@ -4,6 +4,7 @@
 import Propellor.Property.File
 import qualified Propellor.Property.Apt as Apt
 import qualified Propellor.Property.Service as Service
+import qualified Propellor.Property.Systemd as Systemd
 
 import Data.List
 
@@ -40,12 +41,43 @@
 		, "nobody"
 		, "/usr/bin/git"
 		, "git"
+		-- safe.directory configuration is needed by git for
+		-- git-daemon to serve repositories owned by any other user.
+		-- But since git-daemon is explicitly configured which
+		-- repositories to operate on, it adds no security.
+		, "-c", "safe.directory=*"
 		, "daemon"
 		, "--inetd"
 		, "--export-all"
 		, "--base-path=" ++ exportdir
 		, exportdir
 		]
+
+-- | Exports all git repos in a directory (that user nobody can read)
+-- using git-daemon, run as a systemd service.
+daemonService :: FilePath -> Property Linux
+daemonService exportdir = 
+	servicefile `onChange` 
+		Systemd.running "git-daemon"
+  where
+	servicefile = "/etc/systemd/system/git-daemon.service" `hasContent`
+		[ "[Unit]"
+		, "Description=Git Daemon"
+		, "Documentation=man:git-daemon(1)"
+		, ""
+		, " [Service]"
+		-- safe.directory configuration is needed by git for
+		-- git-daemon to serve repositories owned by any other user.
+		-- But since git-daemon is explicitly configured which
+		-- repositories to operate on, it adds no security.
+		, "Environment=GIT_CONFIG_COUNT='1' GIT_CONFIG_KEY_0='safe.directory' GIT_CONFIG_VALUE_0=\"*\""
+		, "ExecStart=/usr/bin/git daemon --export-all --base-path=" ++ exportdir ++ " " ++ exportdir
+		, "DynamicUser=yes"
+		, ""
+		, "[Install]"
+		, "WantedBy=multi-user.target"
+		]
+		`describe` "git-daemon systemd service file"
 
 installed :: Property DebianLike
 installed = Apt.installed ["git"]
diff --git a/src/Propellor/Property/Sbuild.hs b/src/Propellor/Property/Sbuild.hs
--- a/src/Propellor/Property/Sbuild.hs
+++ b/src/Propellor/Property/Sbuild.hs
@@ -21,7 +21,7 @@
 Suggested usage in @config.hs@:
 
 >  mybox = host "mybox.example.com" $ props
->  	& osDebian (Stable "bookworm") X86_64
+>  	& osDebian (Stable "trixie") X86_64
 >  	& Apt.useLocalCacher
 >  	& sidSchrootBuilt
 >  	& Sbuild.usableBy (User "spwhitton")
diff --git a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
--- a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
+++ b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
@@ -183,7 +183,7 @@
 			ARMEL -> "https://github.com/commercialhaskell/stack/releases/download/v1.9.3/stack-1.9.3-linux-arm.tar.gz"
 			-- Newer version because older version did not
 			-- support installing ghc on arm64
-			ARM64 -> "https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-aarch64.tar.gz"
+			ARM64 -> "https://github.com/commercialhaskell/stack/releases/download/v3.3.1/stack-3.3.1-linux-aarch64.tar.gz"
 			-- Probably not available.
 			a -> "https://www.stackage.org/stack/linux-" ++ architectureToDebianArchString a
 	binstack = "/usr/bin/stack"
diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs
--- a/src/Propellor/Property/SiteSpecific/JoeySites.hs
+++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs
@@ -16,7 +16,6 @@
 import qualified Propellor.Property.Service as Service
 import qualified Propellor.Property.User as User
 import qualified Propellor.Property.Group as Group
-import qualified Propellor.Property.Sudo as Sudo
 import qualified Propellor.Property.Borg as Borg
 import qualified Propellor.Property.Apache as Apache
 import qualified Propellor.Property.Postfix as Postfix
@@ -24,10 +23,8 @@
 import qualified Propellor.Property.Network as Network
 import qualified Propellor.Property.Fail2Ban as Fail2Ban
 import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
-import qualified Propellor.Property.Mount as Mount
 import Utility.Split
 
-import Data.List
 import System.Posix.Files
 
 kgbServer :: Property (HasInfo + DebianLike)
@@ -60,7 +57,7 @@
 	& Apt.installed ["kgb-client"]
 	& File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf" anyContext
 		`requires` File.dirExists "/etc/kgb-bot/"
-	& Git.daemonRunning "/srv/git"
+	& Git.daemonService "/srv/git"
 	& "/etc/cgitrc" `File.hasContent`
 		[ "clone-url=https://git.joeyh.name/git/$CGIT_REPO_URL git://git.joeyh.name/$CGIT_REPO_URL"
 		, "css=/cgit-css/cgit.css"
@@ -216,38 +213,11 @@
   where
 	conf = "/home/znc/.znc/configs/znc.conf"
 
-githubBackup :: Property (HasInfo + DebianLike)
-githubBackup = propertyList "github-backup box" $ props
-	& Apt.installed ["github-backup", "moreutils"]
-	& githubKeys
-	& Cron.niceJob "github-backup run" (Cron.Times "30 4 * * *") (User "joey")
-		"/home/joey/lib/backup" backupcmd
-  where
-	backupcmd = intercalate "&&" $
-		[ "mkdir -p github"
-		, "cd github"
-		, ". $HOME/.github-keys"
-		, "github-backup joeyh"
-		]
-
-githubKeys :: Property (HasInfo + UnixLike)
-githubKeys =
-	let f = "/home/joey/.github-keys"
-	in File.hasPrivContent f anyContext
-		`onChange` File.ownerGroup f (User "joey") (Group "joey")
-
-
 rsyncNetBackup :: [Host] -> Property DebianLike
 rsyncNetBackup hosts = Cron.niceJob "rsync.net copied in daily" (Cron.Times "30 5 * * *")
 	(User "joey") "/home/joey/lib/backup" "mkdir -p rsync.net && rsync --delete -az 2318@usw-s002.rsync.net: rsync.net"
 	`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "joey")
 
-podcatcher :: Property DebianLike
-podcatcher = Cron.niceJob "podcatcher run hourly" (Cron.Times "55 * * * *")
-	(User "joey") "/home/joey/lib/sound/podcasts"
-	"timeout 2h xargs git-annex importfeed -c annex.genmetadata=true < feeds; mr --quiet update"
-	`requires` Apt.installed ["git-annex", "myrepos"]
-
 spamdEnabled :: Property DebianLike
 spamdEnabled = Apt.serviceInstalledRunning "spamd"
 
@@ -267,12 +237,19 @@
 		]
 		`describe` "spamassassin configured"
 		`requires` Apt.serviceInstalledRunning "cron"
+	& "/etc/spamassassin/local.cf" `File.containsLines`
+		-- dnswl.org started intentionally sending false responses
+		-- for spam senders when they don't like the amount of
+		-- queries being made. This causes spamassassin to skip
+		-- all other checks and miss flagging spam. This is
+		-- irresponsible behavior, and so they should not be used.
+		[ "dns_query_restriction deny dnswl.org"
+		]
 
 kiteMailServer :: Property (HasInfo + DebianLike)
 kiteMailServer = propertyList "kitenet.net mail server" $ props
 	& Postfix.installed
 	& Apt.installed ["postfix-pcre"]
-	& Apt.serviceInstalledRunning "postgrey"
 	& spamassassinConfigured
 	& Apt.serviceInstalledRunning "spamass-milter"
 	-- Add -m to prevent modifying messages Subject or body.
@@ -281,20 +258,6 @@
 		`onChange` Service.restarted "spamass-milter"
 		`describe` "spamass-milter configured"
 
-	& Apt.serviceInstalledRunning "amavisd-milter"
-	& "/etc/default/amavisd-milter" `File.containsLines`
-		[ "# Propellor deployed"
-		, "MILTERSOCKET=/var/spool/postfix/amavis/amavis.sock"
-		, "MILTERSOCKETOWNER=\"postfix:postfix\""
-		, "MILTERSOCKETMODE=\"0660\""
-		]
-		`onChange` Service.restarted "amavisd-milter"
-		`describe` "amavisd-milter configured for postfix"
-	& Apt.serviceInstalledRunning "clamav-freshclam"
-	-- Workaround https://bugs.debian.org/569150
-	& Cron.niceJob "amavis-expire" Cron.Daily (User "root") "/"
-		"find /var/lib/amavis/virusmails/ -type f -ctime +2 -delete"
-
 	& dkimInstalled
 
 	& Postfix.saslAuthdInstalled
@@ -309,11 +272,8 @@
 		[ "# Global maildrop filter file (deployed with propellor)"
 		, "DEFAULT=\"$HOME/Maildir\""
 		, "MAILBOX=\"$DEFAULT/.\""
-		, "# Filter spam to a spam folder, unless .keepspam exists"
 		, "if (/^X-Spam-Status: Yes/)"
 		, "{"
-		, "  `test -e \"$HOME/.keepspam\"`"
-		, "  if ( $RETURNCODE != 0 )"
 		, "  to ${MAILBOX}spam"
 		, "}"
 		]
@@ -372,11 +332,11 @@
 		, "smtpd_sasl_security_options = noanonymous"
 		, "smtpd_sasl_local_domain = kitenet.net"
 
-		, "# Enable postgrey and sasl auth and client certs."
-		, "smtpd_recipient_restrictions = permit_tls_clientcerts,permit_sasl_authenticated,,permit_mynetworks,reject_unauth_destination,check_policy_service inet:127.0.0.1:10023"
+		, "# Enable sasl auth and client certs."
+		, "smtpd_recipient_restrictions = permit_tls_clientcerts,permit_sasl_authenticated,,permit_mynetworks,reject_unauth_destination"
 
-		, "# Enable spamass-milter, amavis-milter (opendkim is not enabled because it causes mails forwarded from eg gmail to be rejected)"
-		, "smtpd_milters = unix:/spamass/spamass.sock unix:amavis/amavis.sock"
+		, "# Enable spamass-milter (opendkim is not enabled because it causes mails forwarded from eg gmail to be rejected)"
+		, "smtpd_milters = unix:/spamass/spamass.sock"
 		, "# opendkim is used for outgoing mail"
 		, "non_smtpd_milters = inet:localhost:8891"
 		, "milter_connect_macros = j {daemon_name} v {if_name} _"
@@ -389,7 +349,7 @@
 		, "smtpd_tls_key_file = /etc/ssl/private/postfix.pem"
 		, "smtpd_tls_loglevel = 1"
 		, "smtpd_tls_received_header = yes"
-		, "smtpd_use_tls = yes"
+		, "smtpd_tls_security_level = may"
 		, "smtpd_tls_ask_ccert = yes"
 		, "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache"
 
@@ -398,7 +358,7 @@
 		, "smtp_tls_cert_file = /etc/ssl/certs/postfix.pem"
 		, "smtp_tls_key_file = /etc/ssl/private/postfix.pem"
 		, "smtp_tls_loglevel = 1"
-		, "smtp_use_tls = yes"
+		, "smtp_tls_security_level = may"
 		, "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache"
 
 		, "# Allow larger attachments, up to 200 mb."
@@ -422,6 +382,14 @@
 		"!include auth-passwdfile.conf.ext"
 		`onChange` Service.restarted "dovecot"
 		`describe` "dovecot auth.conf"
+	& "/etc/dovecot/conf.d/10-ssl.conf" `File.containsLines`
+		[ "ssl_cert = </etc/letsencrypt/live/kitenet.net/fullchain.pem"
+		, "ssl_key = </etc/letsencrypt/live/kitenet.net/privkey.pem"
+		]
+		`onChange` Service.reloaded "dovecot"
+		`describe` "dovecot ssl.conf using letsencrypt"
+	& Cron.niceJob "dovecot restarts to update letsenctypt certificate"
+		Cron.Daily (User "root") "/" "systemctl restart dovecot"
 	& File.hasPrivContent dovecotusers ctx
 		`onChange` (dovecotusers `File.mode`
 			combineModes [ownerReadMode, groupReadMode])
@@ -519,7 +487,7 @@
 		, "smtp_sasl_auth_enable = yes"
 		, "smtp_tls_security_level = encrypt"
 		, "smtp_sasl_tls_security_options = noanonymous"
-		, "relayhost = kitenet.net:587"
+		, "relayhost = kitenet.net:465"
 		, "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd"
 		]
 		`onChange` Postfix.reloaded
@@ -607,7 +575,7 @@
 		, "rewriterule (.*) http://joeyh.name$1 [r]"
 		]
 	& alias "house.joeyh.name"
-	& apacheSite "house.joeyh.name"
+	& Apache.httpsVirtualHost' "house.joeyh.name" "/srv/web/house.joeyh.name" letos
 		[ "DocumentRoot /srv/web/house.joeyh.name"
 		, "<Directory /srv/web/house.joeyh.name>"
 		, "  Options Indexes ExecCGI"
@@ -729,6 +697,29 @@
 	& "/etc/systemd/logind.conf" `ConfFile.containsIniSetting`
 		("Login", "LidSwitchIgnoreInhibited", "no")
 
+epsolarUsbSetup :: Property UnixLike
+epsolarUsbSetup = propertyList "epsolar usb setup" $ props
+	& "/etc/udev/rules.d/99-epsolar-usb.rules" `File.hasContent`
+		-- Each epever usb cable has its own serial number and gets
+		-- its own stable device name.
+		[ "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"04e2\", ATTRS{idProduct}==\"1411\", ATTRS{serial}==\"G9107376441\", SYMLINK+=\"epsolar1\""
+		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5876071260\", SYMLINK+=\"epsolar2\""
+		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5875017909\", SYMLINK+=\"epsolar3\""
+		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5876071261\", SYMLINK+=\"epsolar4\""
+		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5875017825\", SYMLINK+=\"epsolar5\""
+		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"589A039325\", SYMLINK+=\"epsolar6\""
+		]
+	-- The xr-serial module wants to handle the epever usb cables
+	-- (at least older ones, new ones are handled by ACM), but
+	-- it does not work in linux 6.1. Instead I am currently installing
+	-- an out of tree module, xr_usb_serial_common. In linux 6.6,
+	-- the xr-serial module has been reported to work,
+	-- see https://github.com/kasbert/epsolar-tracer/pull/61#issuecomment-2056442735
+	& "/etc/modprobe.d/blacklist.conf" `File.containsLines`
+		[ "blacklist xr-serial"
+		, "blacklist xr-serial"
+		]
+
 house :: IsContext c => User -> [Host] -> c -> (SshKeyType, Ssh.PubKeyText) -> Property (HasInfo + DebianLike)
 house user hosts ctx sshkey = propertyList "home automation" $ props
 	& Apache.installed
@@ -736,6 +727,7 @@
 	& Git.cloned user "https://git.joeyh.name/git/joey/house.git" d Nothing
 	& websitesymlink
 	& build
+	& epsolarUsbSetup
 	& Systemd.enabled setupservicename
 		`requires` setupserviceinstalled
 		`onChange` Systemd.started setupservicename
@@ -767,7 +759,6 @@
 			user (userGroup user)
 		`requires` File.dirExists (takeDirectory sshkeyfile)
 		`requires` Ssh.knownHost hosts "kitenet.net" user
-	& File.hasPrivContentExposed "/etc/darksky-forecast-url" anyContext
   where
 	d = "/home/joey/house"
 	sshkeyfile = d </> ".ssh/key"
@@ -804,7 +795,7 @@
 		, ""
 		, "[Install]"
 		, "WantedBy=multi-user.target"
-		, "WantedBy=house-controller.target"
+		, "WantedBy=house-controller.service"
 		]
 	controllerservicename = "house-controller"
 	controllerservicefile = "/etc/systemd/system/" ++ controllerservicename ++ ".service"
@@ -853,9 +844,9 @@
 		, ""
 		, "[Install]"
 		, "WantedBy=multi-user.target"
-		, "WantedBy=house-poller.target"
-		, "WantedBy=house-controller.target"
-		, "WantedBy=house-watchdog.target"
+		, "WantedBy=house-poller.service"
+		, "WantedBy=house-controller.service"
+		, "WantedBy=house-watchdog.service"
 		]
 	-- Any changes to the rsync command will need my .authorized_keys
 	-- rsync server command to be updated too.
@@ -878,16 +869,15 @@
 connectStarlinkDish :: Interfaces -> Property DebianLike
 connectStarlinkDish ifs = propertyList "connected via starlink dish" $ props
 	-- Use dhcpcd for ipv6 prefix delegation to the wifi interface.
-	& Apt.installed ["dhcpcd"]
-	-- dhcpcd is run by ifup on boot. When the daemon was enabled,
-	-- that somehow prevented prefix delegation from happening,
-	-- so disable the daemon from being run by systemd.
-	& Systemd.stopped "dhcpcd"
-	& Systemd.masked "dhcpcd"
+	& Apt.installed ["dhcpcd-base"]
+	& Apt.removed ["isc-dhcp-client"]
 	& "/etc/dhcpcd.conf" `File.containsLine`
 		("ia_pd 1 " ++ wifiInterface ifs)
 	& "/etc/dhcpcd.conf" `File.lacksLine`
 		("ia_pd 1 " ++ wifiInterfaceOld ifs)
+	-- Avoid using dns servers sent by starlink.
+	& "/etc/dhcpcd.conf" `File.containsLine`
+		"nohook resolv.conf"
 	& Network.dhcp (ethernetInterface ifs)
 		`requires` Network.cleanInterfacesFile
 
@@ -902,8 +892,8 @@
 		`requires` Network.cleanInterfacesFile
 
 -- My home router, running hostapd and dnsmasq.
-homeRouter :: Interfaces -> String -> HostapdConfig -> Property DebianLike
-homeRouter ifs wifinetworkname (HostapdConfig hostapdconfig) = propertyList "home router" $ props
+homeRouter :: String -> Interfaces -> String -> HostapdConfig -> Property DebianLike
+homeRouter hn ifs wifinetworkname (HostapdConfig hostapdconfig) = propertyList "home router" $ props
 	& File.notPresent (Network.interfaceDFile (wifiInterfaceOld ifs))
 	& Network.static (wifiInterface ifs) (IPv4 "10.1.1.1") Nothing
 		`requires` Network.cleanInterfacesFile
@@ -916,8 +906,9 @@
 		`requires` File.hasContent "/etc/default/hostapd"
 			[ "DAEMON_CONF=/etc/hostapd/hostapd.conf" ]
 		`onChange` Service.running "hostapd"
+	! Systemd.masked "hostapd"
 	& Systemd.enabled "hostapd"
-	& File.hasContent "/etc/resolv.conf"
+	& File.hasContent "/etc/resolv.conf.head"
 		[ "domain kitenet.net"
 		, "search kitenet.net"
 		, "nameserver 8.8.8.8"
@@ -933,7 +924,7 @@
 		-- controller wants to know when clients disconnect
 		, "dhcp-range=10.1.1.100,10.1.1.150,10m"
 		, "no-hosts"
-		, "address=/sky.lan/10.1.1.1"
+		, "address=/" ++ hn ++ "/10.1.1.1"
 		, "address=/house.lan/10.1.1.2"
 		-- allow accessing starlink dish when it's not online yet
 		, "address=/dishy.starlink.com/192.168.100.1"
@@ -960,7 +951,11 @@
 		, "};"
 		]
 		`onChange` Service.restarted "radvd"
-	& "/etc/sysctl.conf" `File.containsLine` "net.ipv6.conf.all.forwarding=1"
+	& "/etc/sysctl.d/ip-forwarding.conf" 
+		`File.containsLine` "net.ipv6.conf.all.forwarding=1"
+	-- Programs that I find useful on a router.
+	& Apt.installed ["net-tools", "wireless-tools"]
+	& Apt.installed ["mtr-tiny", "iftop", "screen", "nmap"]
 
 -- | Enable IP masqerading, on whatever other interfaces come up, besides the
 -- provided intif.
@@ -987,11 +982,12 @@
 laptopSoftware = Apt.installed
 	[ "intel-microcode", "acpi"
 	, "procmeter3", "xfce4", "procmeter3", "unclutter-xfixes"
-	, "mplayer", "fbreader", "firefox", "chromium"
+	, "vlc", "mpv", "mplayer", "fbreader", "foliate", "firefox", "chromium"
+	, "yt-dlp"
 	, "libdatetime-event-sunrise-perl", "libtime-duration-perl"
 	, "network-manager", "network-manager-openvpn-gnome", "openvpn"
-	, "gtk-redshift", "powertop"
-	, "gimp", "gthumb", "inkscape", "sozi", "xzgv", "hugin"
+	, "powertop"
+	, "gimp", "gthumb", "inkscape", "xzgv", "hugin"
 	, "mpc", "mpd", "ncmpc", "sonata", "mpdtoys"
 	, "bsdgames", "nethack-console"
 	, "xmonad", "libghc-xmonad-dev", "libghc-xmonad-contrib-dev"
@@ -999,23 +995,25 @@
 	, "mairix", "offlineimap", "mutt", "slrn"
 	, "mtr", "nmap", "whois", "wireshark", "tcpdump", "iftop"
 	, "pmount", "tree", "pv"
-	, "arbtt", "hledger", "bc"
+	, "hledger", "bc"
 	, "apache2", "ikiwiki", "libhighlight-perl"
 	, "avahi-daemon", "avahi-discover"
-	, "pal"
+	, "pal", "ncal"
 	, "yeahconsole", "xkbset", "xinput"
 	, "assword", "pumpa"
 	, "vorbis-tools", "audacity"
-	, "ekiga"
-	, "bluez-firmware", "blueman", "pulseaudio-module-bluetooth"
+	, "bluez-firmware", "blueman", "pulseaudio-module-bluetooth" 
+	, "pulseaudio-utils"
 	, "fwupd"
-	, "xul-ext-ublock-origin", "xul-ext-pdf.js", "xul-ext-status4evar"
+	, "webext-ublock-origin-firefox", "webext-ublock-origin-chromium"
 	, "vim-syntastic", "vim-fugitive"
 	, "adb", "gthumb"
 	, "w3m", "sm", "weechat"
 	, "borgbackup", "wipe", "smartmontools", "libgfshare-bin"
 	, "units"
-	, "virtualbox", "virtualbox-guest-additions-iso", "qemu-kvm"
+	, "libnotify-bin"
+	, "libmodbus-dev"
+	, "qemu-kvm"
 	]
 	`requires` baseSoftware
 	`requires` devSoftware
@@ -1023,17 +1021,17 @@
 baseSoftware :: Property DebianLike
 baseSoftware = Apt.installed
 	[ "bash", "bash-completion", "vim", "screen", "less", "moreutils"
-	, "git", "mr", "etckeeper", "git-annex", "ssh", "vim-vimoutliner"
+	, "git", "mr", "etckeeper", "git-annex", "ssh" --, "vim-vimoutliner"
 	]
 
 devSoftware :: Property DebianLike
 devSoftware = Apt.installed
 	[ "build-essential", "debhelper", "devscripts"
 	, "ghc", "cabal-install", "haskell-stack"
-	, "hothasktags", "hdevtools", "hlint"
+	, "hasktags", "hlint"
 	, "gdb", "time"
 	, "dpkg-repack", "lintian"
-	, "pristine-tar", "github-backup"
+	, "pristine-tar"
 	]
 
 cubieTruckOneWire :: Property DebianLike
@@ -1072,161 +1070,6 @@
 		, "};"
 		]
 
--- My home networked attached storage server.
-homeNAS :: Property DebianLike
-homeNAS = propertyList "home NAS" $ props
-	& Apt.installed ["uhubctl"]
-	& "/etc/udev/rules.d/52-startech-hub.rules" `File.hasContent`
-		[ "# let users power control startech hub with uhubctl"
-		, "ATTR{idVendor}==\"" ++ hubvendor ++ "\", ATTR{idProduct}==\"005a\", MODE=\"0666\""
-		]
-	& autoMountDrivePort "archive-10"
-		(USBHubPort hubvendor 1)
-		(USBDriveId wd "1230")
-		(Just "archive-oldest")
-	& autoMountDrivePort "archive-11"
-		(USBHubPort hubvendor 2)
-		(USBDriveId wd "25ee")
-		(Just "archive-older")
-	& autoMountDrivePort "archive-12"
-		(USBHubPort hubvendor 3)
-		(USBDriveId seagate "3322")
-		(Just "archive-old")
-	& autoMountDrivePort "archive-13"
-		(USBHubPort hubvendor 4)
-		(USBDriveId wd "25a3")
-		(Just "archive")
-	& autoMountDrivePort "archive-14"
-		(USBHubPort hubvendor 2)
-		(USBDriveId wd "25a3")
-		Nothing
-	& autoMountDrive "passport" Nothing
-	& Apt.installed ["git-annex", "borgbackup"]
-  where
-	hubvendor = "0409"
-	wd = "1058"
-	seagate = "0bc2"
-
-data USBHubPort = USBHubPort
-	{ hubVendor :: String
-	, hubPort :: Int
-	}
-
-data USBDriveId = USBDriveId
-	{ driveVendorId :: String
-	, driveProductId :: String
-	}
-
--- Makes a USB drive with the given label automount, and unmount after idle
--- for a while.
---
--- The hub port is turned on and off automatically as needed, using
--- uhubctl.
-autoMountDrivePort :: Mount.Label -> USBHubPort -> USBDriveId -> Maybe FilePath -> Property DebianLike
-autoMountDrivePort label hp drive malias = propertyList desc $ props
-	& File.hasContent ("/etc/systemd/system/" ++ hub)
-		[ "[Unit]"
-		, "Description=Startech usb hub port " ++ show (hubPort hp) ++ " vendor " ++ driveVendorId drive ++ " driveid " ++ driveProductId drive
-		, "PartOf=" ++ mount
-		, "[Service]"
-		, "Type=oneshot"
-		, "RemainAfterExit=true"
-		, "ExecStart=/bin/sh -c 'uhubctl -a on " ++ selecthubport ++ "'"
-		, "ExecStop=/bin/sh -c 'uhubctl -a off " ++ selecthubport
-			-- Powering off the port does not remove device
-			-- files, so ask udev to remove the devfile; it will
-			-- be added back after the drive next spins up
-			-- and so avoid mount happening before the drive is
-			-- spun up.
-			-- (This only works when the devfile is in
-			-- by-label.)
-			++ "; udevadm trigger --action=remove " ++ devfile ++ " || true'"
-		, "[Install]"
-		, "WantedBy="
-		]
-		`onChange` Systemd.daemonReloaded
-	& autoMountDrive' 
-		[ "Requires=" ++ hub
-		, "After=" ++ hub
-		] label malias
-  where
-	devfile = "/dev/disk/by-label/" ++ label
-	mountpoint = "/media/joey/" ++ label
-	desc = "auto mount with hub port power control " ++ mountpoint
-	hub = "startech-hub-port-" ++ show (hubPort hp) ++ "-vendor-" ++ driveVendorId drive ++ "-drivedid-" ++ driveProductId drive ++ ".service"
-	mount = svcbase ++ ".mount"
-	svcbase = Systemd.escapePath mountpoint
-	selecthubport = unwords
-		[ "-p", show (hubPort hp)
-		, "-n", hubVendor hp
-		, "-l", concat
-			-- The hub's location id, eg "1-1.4", does not seem
-			-- as stable as uhubctl claims it will be,
-			-- and the vendor is not sufficient since I have 2
-			-- hubs from the same vendor. So search for the
-			-- drive lsusb to find that. This works even if the
-			-- port is powered off, as long as it's been on at
-			-- some point before.
-			[ "$(lsusb -tvv | perl -lne \"if (\\\\$h && m!/sys/bus/usb/devices/(.*?) !) {\\\\$v=\\\\$1}; if (m/Hub/) { \\\\$h=1 } else { \\\\$h=0 }; if (/"
-			, driveVendorId drive ++ ":" ++ driveProductId drive
-			++ "/) { print \\\\$v; last}\")"
-			]
-		]
-
--- Makes a USB drive with the given label automount, and unmount after idle
--- for a while.
-autoMountDrive :: Mount.Label -> Maybe FilePath -> Property DebianLike
-autoMountDrive = autoMountDrive' []
-
-autoMountDrive' :: [String] -> Mount.Label -> Maybe FilePath -> Property DebianLike
-autoMountDrive' mountunitadd label malias = propertyList desc $ props
-	& File.ownerGroup mountpoint (User "joey") (Group "joey")
-		`requires` File.dirExists mountpoint
-	& case malias of
-		Just t -> ("/media/joey/" ++ t) `File.isSymlinkedTo`
-			File.LinkTarget mountpoint
-		Nothing -> doNothing <!> doNothing
-	& File.hasContent ("/etc/systemd/system/" ++ mount)
-		([ "[Unit]"
-		, "Description=" ++ label
-		] ++ mountunitadd ++
-		[ "[Mount]"
-		-- avoid mounting whenever the block device is available,
-		-- only want to automount on demand
-		, "Options=noauto"
-		, "What=" ++ devfile
-		, "Where=" ++ mountpoint
-		, "[Install]"
-		, "WantedBy="
-		])
-		`onChange` Systemd.daemonReloaded
-	& File.hasContent ("/etc/systemd/system/" ++ automount)
-		[ "[Unit]"
-		, "Description=Automount " ++ label
-		, "[Automount]"
-		, "Where=" ++ mountpoint
-		, "TimeoutIdleSec=300"
-		, "[Install]"
-		, "WantedBy=multi-user.target"
-		]
-		`onChange` Systemd.daemonReloaded
-	& Systemd.enabled automount
-	& Systemd.started automount
-	& Sudo.sudoersDFile ("automount-" ++ label)
-		[ "joey ALL= NOPASSWD: " ++ sudocommands
-		]
-  where
-	devfile = "/dev/disk/by-label/" ++ label
-	mountpoint = "/media/joey/" ++ label
-	desc = "auto mount " ++ mountpoint
-	automount = svcbase ++ ".automount"
-	mount = svcbase ++ ".mount"
-	svcbase = Systemd.escapePath mountpoint
-	sudocommands = intercalate " , " $ map (\c -> "/bin/systemctl " ++ c)
-		[ "stop " ++ mountpoint
-		, "start " ++ mountpoint
-		]
-
 rsyncNetBorgRepo :: String -> [Borg.BorgRepoOpt] -> Borg.BorgRepo
 rsyncNetBorgRepo d os = Borg.BorgRepoUsing os' ("2318@usw-s002.rsync.net:" ++ d)
   where
@@ -1252,7 +1095,7 @@
 -- For wifi adapters such as the Alfa AWUS036ACHM
 --
 -- Note that for maximum speed, this needs channel 5 or 6.
--- This should make it be capable of 150 Mb/s.
+-- This should make it be capable of 300 Mb/s.
 hostapd2GhzConfig_mt76 :: HostapdConfig
 hostapd2GhzConfig_mt76 = HostapdConfig $ c ++ 
 	[ "ht_capab=[HT40+][HT40-][GF][SHORT-GI-20][SHORT-GI-40]"
@@ -1270,4 +1113,18 @@
 	, "ieee80211ac=1"
 	, "wmm_enabled=1"
 	]
+
+thinkPadLedsOff :: Property Linux
+thinkPadLedsOff =
+	"/etc/systemd/system/thinkpad_leds_off.service" `File.hasContent`
+		[ "[Unit]"
+		, "After=suspend.target"
+		, "[Service]"
+		, "Type=simple"
+		, "ExecStart=sh -c 'echo 0 > /sys/devices/platform/thinkpad_acpi/leds/tpacpi::lid_logo_dot/brightness; echo 0 > /sys/devices/platform/thinkpad_acpi/leds/tpacpi::power/brightness'"
+		, "[Install]"
+		, "WantedBy=suspend.target"
+		]
+		`onChange` (Systemd.daemonReloaded
+			`before` Systemd.enabled "thinkpad_leds_off")
 
diff --git a/src/Propellor/Property/Systemd.hs b/src/Propellor/Property/Systemd.hs
--- a/src/Propellor/Property/Systemd.hs
+++ b/src/Propellor/Property/Systemd.hs
@@ -314,6 +314,12 @@
 		, "--boot"
 		, "--directory=" ++ containerDir name
 		, "--machine=" ++ name
+		-- While propellChroot bind mounts the propellor directory,
+		-- when systemd stops and then restarts the machine, it
+		-- will mask any existing bind mounts to the directory.
+		-- So, tell systemd to bind mount the propellor directory
+		-- as well.
+		, "--bind=" ++ localdir
 		] ++ nspawnServiceParams cfg
 
 	overrideconfigured = File.hasContent overridefile overridecontent
diff --git a/src/Propellor/Types/OS.hs b/src/Propellor/Types/OS.hs
--- a/src/Propellor/Types/OS.hs
+++ b/src/Propellor/Types/OS.hs
@@ -59,7 +59,7 @@
 	deriving (Show, Eq)
 
 -- | Debian has several rolling suites, and a number of stable releases,
--- such as Stable "bookworm".
+-- such as Stable "trixie".
 data DebianSuite = Experimental | Unstable | Testing | Stable Release
 	deriving (Show, Eq)
 
