diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,26 @@
+propellor (5.5.0) unstable; urgency=medium
+
+  * letsencrypt': Pass --expand to support expanding the list of domains
+  * Split mailname property out of Hostname.sane, since bad mailname
+    guesses can lead to ugly surprises. (API change)
+  * Removed HostingProvider.CloudatCost module as it lacks a maintainer.
+    (If anyone would like to maintain it, send a patch adding it back.)
+    (API change)
+  * Added Systemd.escapePath helper function useful when creating mount
+    units.
+  * Added Sudo.sudoersDFile property.
+  * Sudo.enabledFor: Write to /etc/sudoers.d/000users rather than to
+    /etc/sudoers. (Any old lines it wrote to /etc/sudoers will be removed.)
+    This fixes a potential ordering problem; the property used to append
+    the line to /etc/sudoers, but that would override more specific lines
+    in the include directory.
+  * Borg: Added UsesEnvVar.
+  * Added DiskImage.noBootloader, useful for eg, direct booting with
+    qemu. Thanks, David Bremner.
+  * Added Apt.backportInstalledMin.
+
+ -- Joey Hess <id@joeyh.name>  Sat, 20 Oct 2018 21:00:27 -0400
+
 propellor (5.4.1) unstable; urgency=medium
 
   * Modernized and simplified the MetaTypes implementation now that
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,26 @@
+propellor (5.5.0) unstable; urgency=medium
+
+  * letsencrypt': Pass --expand to support expanding the list of domains
+  * Split mailname property out of Hostname.sane, since bad mailname
+    guesses can lead to ugly surprises. (API change)
+  * Removed HostingProvider.CloudatCost module as it lacks a maintainer.
+    (If anyone would like to maintain it, send a patch adding it back.)
+    (API change)
+  * Added Systemd.escapePath helper function useful when creating mount
+    units.
+  * Added Sudo.sudoersDFile property.
+  * Sudo.enabledFor: Write to /etc/sudoers.d/000users rather than to
+    /etc/sudoers. (Any old lines it wrote to /etc/sudoers will be removed.)
+    This fixes a potential ordering problem; the property used to append
+    the line to /etc/sudoers, but that would override more specific lines
+    in the include directory.
+  * Borg: Added UsesEnvVar.
+  * Added DiskImage.noBootloader, useful for eg, direct booting with
+    qemu. Thanks, David Bremner.
+  * Added Apt.backportInstalledMin.
+
+ -- Joey Hess <id@joeyh.name>  Sat, 20 Oct 2018 21:00:27 -0400
+
 propellor (5.4.1) unstable; urgency=medium
 
   * Modernized and simplified the MetaTypes implementation now that
diff --git a/joeyconfig.hs b/joeyconfig.hs
--- a/joeyconfig.hs
+++ b/joeyconfig.hs
@@ -11,7 +11,6 @@
 import qualified Propellor.Property.File as File
 import qualified Propellor.Property.Apt as Apt
 import qualified Propellor.Property.Network as Network
-import qualified Propellor.Property.Service as Service
 import qualified Propellor.Property.Ssh as Ssh
 import qualified Propellor.Property.Cron as Cron
 import qualified Propellor.Property.Sudo as Sudo
@@ -20,7 +19,6 @@
 import qualified Propellor.Property.Fstab as Fstab
 import qualified Propellor.Property.Tor as Tor
 import qualified Propellor.Property.Dns as Dns
-import qualified Propellor.Property.OpenId as OpenId
 import qualified Propellor.Property.Git as Git
 import qualified Propellor.Property.Postfix as Postfix
 import qualified Propellor.Property.Apache as Apache
@@ -53,8 +51,8 @@
 	, baleen
 	, honeybee
 	, kite
-	, elephant
 	, beaver
+	, sow
 	, mouse
 	, peregrine
 	, pell
@@ -66,6 +64,7 @@
 	& osDebian Unstable X86_64
 	& ipv6 "2001:4830:1600:187::2"
 	& Hostname.sane
+	& Hostname.mailname
 	& Apt.serviceInstalledRunning "swapspace"
 	& Laptop.powertopAutoTuneOnBoot
 	& Laptop.trimSSD
@@ -79,12 +78,12 @@
 	& JoeySites.laptopSoftware
 	& JoeySites.userDirHtml
 	& Ssh.userKeys (User "joey") hostContext
-		[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1YoyHxZwG5Eg0yiMTJLSWJ/+dMM6zZkZiR4JJ0iUfP+tT2bm/lxYompbSqBeiCq+PYcSC67mALxp1vfmdOV//LWlbXfotpxtyxbdTcQbHhdz4num9rJQz1tjsOsxTEheX5jKirFNC5OiKhqwIuNydKWDS9qHGqsKcZQ8p+n1g9Lr3nJVGY7eRRXzw/HopTpwmGmAmb9IXY6DC2k91KReRZAlOrk0287LaK3eCe1z0bu7LYzqqS+w99iXZ/Qs0m9OqAPnHZjWQQ0fN4xn5JQpZSJ7sqO38TBAimM+IHPmy2FTNVVn9zGM+vN1O2xr3l796QmaUG1+XLL0shfR/OZbb joey@darkstar")
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfFntnesZcYz2B2T41ay45igfckXRSh5uVffkuCQkLv joey@darkstar")
 		]
-	-- & imageBuiltFor honeybee
-	--	(RawDiskImage "/srv/honeybee.img")
-	--	(Debootstrapped mempty)
-	& imageBuiltFor banana
+	& imageBuiltFor honeybee
+		(RawDiskImage "/srv/honeybee.img")
+		(Debootstrapped mempty)
+	! imageBuiltFor banana
 		(RawDiskImage "/srv/banana.img")
 		(Debootstrapped mempty)
 
@@ -156,8 +155,6 @@
 	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
 		GitAnnexBuilder.stackAutoBuilder
 		(Stable "jessie") X86_32 (Just "ancient") (Cron.Times "45 * * * *") "2h")
-	& Systemd.nspawned (GitAnnexBuilder.androidAutoBuilderContainer
-		(Cron.Times "1 1 * * *") "3h")
 
 banana :: Host
 banana = host "banana.kitenet.net" $ props
@@ -178,17 +175,17 @@
 	
 	& cubietech_Cubietruck
 	& hasPartition
-		( partition EXT4
+		( partition EXT3
 			`mountedAt` "/"
-			`setSize` MegaBytes 8000
+			`setSize` MegaBytes 16000
 		)
 	& JoeySites.cubieTruckOneWire
 	
+	& Apt.installed ["firmware-misc-nonfree"]
 	& Apt.installed ["firmware-brcm80211"]
 		-- Workaround for https://bugs.debian.org/844056
 		`requires` File.hasPrivContent "/lib/firmware/brcm/brcmfmac43362-sdio.txt" anyContext
 		`requires` File.dirExists "/lib/firmware/brcm"
-	& "/etc/default/rcS" `File.containsLine` "FSCKFIX=yes"
 	& Apt.serviceInstalledRunning "ntp" -- no hardware clock
 	& bootstrappedFrom GitRepoOutsideChroot
 	& Ssh.hostKeys hostContext
@@ -201,16 +198,21 @@
 		(Context "homepower.joeyh.name")
 		(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMAmVYddg/RgCbIj+cLcEiddeFXaYFnbEJ3uGj9G/EyV joey@honeybee")
 	& JoeySites.homeRouter
+	& JoeySites.homeNAS
 	& Apt.installed ["mtr-tiny", "iftop", "screen"]
 	& Postfix.satellite
 
 	& check (not <$> inChroot) (setupRevertableProperty autobuilder)
+	& check (not <$> inChroot) (undoRevertableProperty ancientautobuilder)
 	-- In case compiler needs more than available ram
 	& Apt.serviceInstalledRunning "swapspace"
   where
 	autobuilder = Systemd.nspawned $ GitAnnexBuilder.autoBuilderContainer
-		GitAnnexBuilder.armAutoBuilder
+		(GitAnnexBuilder.armAutoBuilder GitAnnexBuilder.standardAutoBuilder)
 		Unstable ARMEL Nothing (Cron.Times "15 15 * * *") "10h"
+	ancientautobuilder = Systemd.nspawned $ GitAnnexBuilder.autoBuilderContainer
+		(GitAnnexBuilder.armAutoBuilder GitAnnexBuilder.stackAutoBuilder)
+		(Stable "jessie") ARMEL (Just "ancient") (Cron.Times "5 15 * * *") "10h"
 
 -- This is not a complete description of kite, since it's a
 -- multiuser system with eg, user passwords that are not deployed
@@ -244,13 +246,16 @@
 	& Apt.serviceInstalledRunning "ntp"
 	& "/etc/timezone" `File.hasContent` ["US/Eastern"]
 	
-	& Borg.backup "/" (Borg.BorgRepo "joey@eubackup.kitenet.net:/home/joey/lib/backup/kite/kite.borg") Cron.Daily
+	& Borg.backup "/" (JoeySites.rsyncNetBorgRepo "kite.borg" []) Cron.Daily
 		[ "--exclude=/proc/*"
 		, "--exclude=/sys/*"
 		, "--exclude=/run/*"
+		, "--exclude=/mnt/*"
 		, "--exclude=/tmp/*"
 		, "--exclude=/var/tmp/*"
 		, "--exclude=/var/cache/*"
+		, "--exclude=/var/lib/swapspace/*"
+		, "--exclude=/var/lib/container/*"
 		, "--exclude=/home/joey/lib"
 		-- These directories are backed up and restored separately.
 		, "--exclude=/srv/git"
@@ -260,7 +265,7 @@
 		, Borg.KeepWeeks 4
 		, Borg.KeepMonths 6
 		]
-		`requires` Ssh.knownHost hosts "eubackup.kitenet.net" (User "root")
+		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
 		`requires` Ssh.userKeys (User "root")
 			(Context "kite.kitenet.net")
 			[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5Gza2sNqSKfNtUN4dN/Z3rlqw18nijmXFx6df2GtBoZbkIak73uQfDuZLP+AXlyfHocwdkdHEf/zrxgXS4EokQMGLZhJ37Pr3edrEn/NEnqroiffw7kyd7EqaziA6UOezcLTjWGv+Zqg9JhitYs4WWTpNzrPH3yQf1V9FunZnkzb4gJGndts13wGmPEwSuf+QHbgQvjMOMCJwWSNcJGdhDR66hFlxfG26xx50uIczXYAbgLfHp5W6WuR/lcaS9J6i7HAPwcsPDA04XDinrcpl29QwsMW1HyGS/4FSCgrDqNZ2jzP49Bka78iCLRqfl1efyYas/Zo1jQ0x+pxq2RMr root@kite")
@@ -280,22 +285,10 @@
 	& alias "git.joeyh.name"
 	& JoeySites.gitServer hosts
 
-	& JoeySites.downloads hosts
+	& JoeySites.downloads
 	& JoeySites.gitAnnexDistributor
 	& JoeySites.tmp
 
-	& alias "bitlbee.kitenet.net"
-	& Apt.serviceInstalledRunning "bitlbee"
-	& "/etc/bitlbee/bitlbee.conf" `File.hasContent`
-		[ "[settings]"
-		, "User = bitlbee"
-		, "AuthMode = Registered"
-		, "[defaults]"
-		]
-		`onChange` Service.restarted "bitlbee"
-	& "/etc/default/bitlbee" `File.containsLine` "BITLBEE_PORT=\"6767\""
-		`onChange` Service.restarted "bitlbee"
-
 	& Apt.installed
 		[ "git-annex", "myrepos"
 		, "build-essential", "make"
@@ -308,7 +301,18 @@
 	& alias "nntp.olduse.net"
 	& JoeySites.oldUseNetServer hosts
 	& Systemd.nspawned oldusenetShellBox
+	
+	& alias "znc.kitenet.net"
+	& JoeySites.ircBouncer
 
+	& alias "kgb.kitenet.net"
+	& JoeySites.kgbServer
+	
+	& Systemd.nspawned ancientKitenet
+	
+	& alias "podcatcher.kitenet.net"
+	& JoeySites.podcatcher
+
 	& JoeySites.scrollBox
 	& alias "scroll.joeyh.name"
 	& alias "us.scroll.joeyh.name"
@@ -318,6 +322,7 @@
 		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
 		, (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
 		, (RelDomain "peregrine-onion", CNAME $ AbsDomain "ahw47zqw6qszoufl.onion")
+		, (RelDomain "sow-onion", CNAME $ AbsDomain "urt4g2tq32qktgtp.onion")
 		]
 	& myDnsPrimary "joeyh.name" []
 	& myDnsPrimary "ikiwiki.info" []
@@ -334,6 +339,7 @@
 		, "domain kitenet.net"
 		, "search kitenet.net"
 		]
+	
 	& alias "debug-me.joeyh.name"
 	& Apt.installed ["debug-me"]
 	& Systemd.enabled "debug-me"
@@ -342,79 +348,21 @@
 	& Apache.httpsVirtualHost "letsencrypt.joeyh.name" "/var/www/html"
 		(LetsEncrypt.AgreeTOS (Just "id@joeyh.name"))
 	& alias "letsencrypt.joeyh.name"
-  where
 
-elephant :: Host
-elephant = host "elephant.kitenet.net" $ props
-	& standardSystem Unstable X86_64
-		[ "Storage, big data, and backups, omnomnom!"
-		, "(Encrypt all data stored here.)"
-		]
-	& ipv4 "193.234.225.114"
-	& Ssh.hostKeys hostContext
-		[ (SshDsa, "ssh-dss AAAAB3NzaC1kc3MAAACBANxXGWac0Yz58akI3UbLkphAa8VPDCGswTS0CT3D5xWyL9OeArISAi/OKRIvxA4c+9XnWtNXS7nYVFDJmzzg8v3ZMx543AxXK82kXCfvTOc/nAlVz9YKJAA+FmCloxpmOGrdiTx1k36FE+uQgorslGW/QTxnOcO03fDZej/ppJifAAAAFQCnenyJIw6iJB1+zuF/1TSLT8UAeQAAAIEA1WDrI8rKnxnh2rGaQ0nk+lOcVMLEr7AxParnZjgC4wt2mm/BmkF/feI1Fjft2z4D+V1W7MJHOqshliuproxhFUNGgX9fTbstFJf66p7h7OLAlwK8ZkpRk/uV3h5cIUPel6aCwjL5M2gN6/yq+gcCTXeHLq9OPyUTmlN77SBL71UAAACBAJJiCHWxPAGooe7Vv3W7EIBbsDyf7b2kDH3bsIlo+XFcKIN6jysBu4kn9utjFlrlPeHUDzGQHe+DmSqTUQQ0JPCRGcAcuJL8XUqhJi6A6ye51M9hVt51cJMXmERx9TjLOP/adkEuxpv3Fj20FxRUr1HOmvRvewSHrJ1GeA1bjbYL")
-		, (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrEQ7aNmRYyLKY7xHILQsyV/w0B3++D98vn5IvjHkDnitrUWjB+vPxlS7LYKLzN9Jx7Hb14R2lg7+wdgtFMxLZZukA8b0tqFpTdRFBvBYGh8IM8Id1iE/6io/NZl+hTQEDp0LJP+RljH1CLfz7J3qtc+v6NbfTP5cOgH104mWYoLWzJGaZ4p53jz6THRWnVXy5nPO3dSBr2f/SQgRuJQWHNIh0jicRGD8H2kzOQzilpo+Y46PWtkufl3Yu3UsP5UMAyLRIXwZ6nNRZqRiVWrX44hoNfDbooTdFobbHlqMl+y6291bOXaOA6PACk8B4IVcC89/gmc9Oe4EaDuszU5kD")
-		, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJkoPRhUGT8EId6m37uBdYEtq42VNwslKnc9mmO+89ody066q6seHKeFY6ImfwjcyIjM30RTzEwftuVNQnbEB0=")
-		, (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB6VtXi0uygxZeCo26n6PuCTlSFCBcwRifv6N8HdWh2Z")
-		]
-
-	& Grub.chainPVGrub "hd0,0" "xen/xvda1" 30
-	& Postfix.satellite
-	& Apt.unattendedUpgrades
-	& Systemd.installed
-	& Systemd.persistentJournal
-	& Ssh.userKeys (User "joey") hostContext
-		[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4wJuQEGno+nJvtE75IKL6JQ08sJHZ9Bzs9Dvu0zuxSEZE30MWK98/twNwCH9PVf2N9m4apfN7f9GHgHTUongfo8xnLAk4PuBSTV74YgKyOCvNYqANuKKa+76PsS/vFf/or3ct++uTEWsRyYD29cQndufwKA4rthAqHG+fifbLDC53AjcldI0zI1RckpPzT+AMazlnSBFMlpKvGD2uzSXALVRXa3vSqWkWd0z7qmIkpmpq0AAgbDLwrGBcUGV/h0rOa2s8zSeirA0tLmHNROl4cZsX0T/6VBGfBRkrHSxL67xJziATw4WPq6spYlxg84pC/5qJVr9SC5HosppbDqgj joey@elephant")
-		]
-	& Apt.serviceInstalledRunning "swapspace"
-
-	& alias "eubackup.kitenet.net"
-	& Apt.installed ["sshfs", "rsync", "borgbackup"]
-	& JoeySites.githubBackup
-	& JoeySites.rsyncNetBackup hosts
-
-	& alias "podcatcher.kitenet.net"
-	& JoeySites.podcatcher
-
-	& alias "znc.kitenet.net"
-	& JoeySites.ircBouncer
-	& alias "kgb.kitenet.net"
-	& JoeySites.kgbServer
-
-	& alias "ns3.kitenet.net"
-	& myDnsSecondary
-
-	& Systemd.nspawned oldusenetShellBox
-	& Systemd.nspawned ancientKitenet
-	& Systemd.nspawned openidProvider
-	 	`requires` Apt.serviceInstalledRunning "ntp"
-
-	& JoeySites.scrollBox
-	& alias "scroll.joeyh.name"
-	& alias "eu.scroll.joeyh.name"
-
-	-- For https port 443, shellinabox with ssh login to
-	-- kitenet.net
-	& alias "shell.kitenet.net"
-	& Systemd.nspawned kiteShellBox
-	-- Nothing is using http port 80, so listen on
-	-- that port for ssh, for traveling on bad networks that
-	-- block 22.
-	& Ssh.listenPort (Port 80)
-
 beaver :: Host
 beaver = host "beaver.kitenet.net" $ props
-	& ipv6 "2001:4830:1600:195::2"
 	& Apt.installed ["ssh"]
 	& Ssh.hostPubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAACBAIrLX260fY0Jjj/p0syNhX8OyR8hcr6feDPGOj87bMad0k/w/taDSOzpXe0Wet7rvUTbxUjH+Q5wPd4R9zkaSDiR/tCb45OdG6JsaIkmqncwe8yrU+pqSRCxttwbcFe+UU+4AAcinjVedZjVRDj2rRaFPc9BXkPt7ffk8GwEJ31/AAAAFQCG/gOjObsr86vvldUZHCteaJttNQAAAIB5nomvcqOk/TD07DLaWKyG7gAcW5WnfY3WtnvLRAFk09aq1EuiJ6Yba99Zkb+bsxXv89FWjWDg/Z3Psa22JMyi0HEDVsOevy/1sEQ96AGH5ijLzFInfXAM7gaJKXASD7hPbVdjySbgRCdwu0dzmQWHtH+8i1CMVmA2/a5Y/wtlJAAAAIAUZj2US2D378jBwyX1Py7e4sJfea3WSGYZjn4DLlsLGsB88POuh32aOChd1yzF6r6C2sdoPBHQcWBgNGXcx4gF0B5UmyVHg3lIX2NVSG1ZmfuLNJs9iKNu4cHXUmqBbwFYQJBvB69EEtrOw4jSbiTKwHFmqdA/mw1VsMB+khUaVw=="
 	& Tor.installed
 	& Tor.hiddenServiceAvailable "ssh" (Port 22)
-	& alias "usbackup.kitenet.net"
-	& JoeySites.backupsBackedupFrom hosts "eubackup.kitenet.net" "/home/joey/lib/backup"
-	& Apt.serviceInstalledRunning "anacron"
-	& Cron.niceJob "system disk backed up" Cron.Weekly (User "root") "/"
-		"rsync -a -x / /home/joey/lib/backup/beaver.kitenet.net/"
 
+sow :: Host
+sow = host "sow.kitenet.net" $ props
+	& Apt.installed ["ssh"]
+	& Ssh.hostPubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAACBAIrLX260fY0Jjj/p0syNhX8OyR8hcr6feDPGOj87bMad0k/w/taDSOzpXe0Wet7rvUTbxUjH+Q5wPd4R9zkaSDiR/tCb45OdG6JsaIkmqncwe8yrU+pqSRCxttwbcFe+UU+4AAcinjVedZjVRDj2rRaFPc9BXkPt7ffk8GwEJ31/AAAAFQCG/gOjObsr86vvldUZHCteaJttNQAAAIB5nomvcqOk/TD07DLaWKyG7gAcW5WnfY3WtnvLRAFk09aq1EuiJ6Yba99Zkb+bsxXv89FWjWDg/Z3Psa22JMyi0HEDVsOevy/1sEQ96AGH5ijLzFInfXAM7gaJKXASD7hPbVdjySbgRCdwu0dzmQWHtH+8i1CMVmA2/a5Y/wtlJAAAAIAUZj2US2D378jBwyX1Py7e4sJfea3WSGYZjn4DLlsLGsB88POuh32aOChd1yzF6r6C2sdoPBHQcWBgNGXcx4gF0B5UmyVHg3lIX2NVSG1ZmfuLNJs9iKNu4cHXUmqBbwFYQJBvB69EEtrOw4jSbiTKwHFmqdA/mw1VsMB+khUaVw=="
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
 mouse :: Host
 mouse = host "mouse.kitenet.net" $ props
 	& ipv4 "67.223.19.96"
@@ -461,6 +409,7 @@
 keysafe = host "keysafe.joeyh.name" $ props
 	& ipv4 "139.59.17.168"
 	& Hostname.sane
+	& Hostname.mailname
 	& osDebian (Stable "stretch") X86_64
 	& Apt.stdSourcesList `onChange` Apt.upgrade
 	& Apt.unattendedUpgrades
@@ -517,16 +466,6 @@
 --------------------------- \____, o          ,' ----------------------------
 ---------------------------- '--,___________,'  -----------------------------
 
--- My own openid provider. Uses php, so containerized for security
--- and administrative sanity.
-openidProvider :: Systemd.Container
-openidProvider = Systemd.debContainer "openid-provider" $ props
-	& standardContainer (Stable "stretch")
-	& alias hn
-	& OpenId.providerFor [User "joey", User "liw"] hn (Just (Port 8081))
-  where
-	hn = "openid.kitenet.net"
-
 -- Exhibit: kite's 90's website on port 1994.
 ancientKitenet :: Systemd.Container
 ancientKitenet = Systemd.debContainer "ancient-kitenet" $ props
@@ -548,11 +487,6 @@
 	& alias "shell.olduse.net"
 	& JoeySites.oldUseNetShellBox
 
-kiteShellBox :: Systemd.Container
-kiteShellBox = Systemd.debContainer "kiteshellbox" $ props
-	& standardContainer (Stable "stretch")
-	& JoeySites.kiteShellBox
-
 type Motd = [String]
 
 -- This is my standard system setup.
@@ -565,6 +499,7 @@
 standardSystemUnhardened suite arch motd = propertyList "standard system" $ props
 	& osDebian suite arch
 	& Hostname.sane
+	& Hostname.mailname
 	& Hostname.searchDomain
 	& Locale.available "en_US.UTF-8"
 	& File.hasContent "/etc/motd" ("":motd++[""])
@@ -602,22 +537,22 @@
 branchableSecondary :: RevertableProperty (HasInfo + DebianLike) DebianLike
 branchableSecondary = Dns.secondaryFor ["branchable.com"] hosts "branchable.com"
 
--- Currently using kite (ns4) as primary with secondaries
--- elephant (ns3) and gandi.
+-- Currently using kite (ns4) as primary with gandi as secondary
 -- kite handles all mail.
 myDnsPrimary :: Domain -> [(BindDomain, Record)] -> RevertableProperty (HasInfo + DebianLike) DebianLike
 myDnsPrimary domain extras = Dns.signedPrimary (Weekly Nothing) hosts domain
 	(Dns.mkSOA "ns4.kitenet.net" 100) $
 	[ (RootDomain, NS $ AbsDomain "ns4.kitenet.net")
-	, (RootDomain, NS $ AbsDomain "ns3.kitenet.net")
 	, (RootDomain, NS $ AbsDomain "ns6.gandi.net")
 	, (RootDomain, MX 0 $ AbsDomain "kitenet.net")
 	, (RootDomain, TXT "v=spf1 a a:kitenet.net ~all")
 	, JoeySites.domainKey
 	] ++ extras
 
-monsters :: [Host]    -- Systems I don't manage with propellor,
-monsters =            -- but do want to track their public keys etc.
+-- Systems I don't manage with propellor,
+-- but do want to track their public keys etc.
+monsters :: [Host]
+monsters =
 	[ host "usw-s002.rsync.net" $ props
 		& Ssh.hostPubKey SshEd25519 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB7yTEBGfQYdwG/oeL+U9XPMIh/dW7XNs9T+M79YIOrd"
 	, host "github.com" $ props
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 5.4.1
+Version: 5.5.0
 Cabal-Version: 1.20
 License: BSD2
 Maintainer: Joey Hess <id@joeyh.name>
@@ -144,7 +144,6 @@
     Propellor.Property.ZFS
     Propellor.Property.ZFS.Process
     Propellor.Property.ZFS.Properties
-    Propellor.Property.HostingProvider.CloudAtCost
     Propellor.Property.HostingProvider.DigitalOcean
     Propellor.Property.HostingProvider.Exoscale
     Propellor.Property.HostingProvider.Linode
diff --git a/src/Propellor/Property/Apt.hs b/src/Propellor/Property/Apt.hs
--- a/src/Propellor/Property/Apt.hs
+++ b/src/Propellor/Property/Apt.hs
@@ -241,6 +241,10 @@
 installed :: [Package] -> Property DebianLike
 installed = installed' ["-y"]
 
+-- | Minimal install of package, without recommends.
+installedMin :: [Package] -> Property DebianLike
+installedMin = installed' ["--no-install-recommends", "-y"]
+
 installed' :: [String] -> [Package] -> Property DebianLike
 installed' params ps = robustly $ check (not <$> isInstalled' ps) go
 	`describe` unwords ("apt installed":ps)
@@ -253,19 +257,22 @@
 -- dependencies from stable-backports too, you will need to include those
 -- dependencies in the list of packages passed to this function.
 backportInstalled :: [Package] -> Property Debian
-backportInstalled ps = withOS desc $ \w o -> case o of
+backportInstalled = backportInstalled' ["-y"]
+
+-- | Minimal install from the stable-backports suite, without recommends.
+backportInstalledMin :: [Package] -> Property Debian
+backportInstalledMin = backportInstalled' ["--no-install-recommends", "-y"]
+
+backportInstalled' :: [String] -> [Package] -> Property Debian
+backportInstalled' params ps = withOS desc $ \w o -> case o of
 	(Just (System (Debian _ suite) _)) -> case backportSuite suite of
 		Nothing -> unsupportedOS'
 		Just bs -> ensureProperty w $
-			runApt (["install", "-y"] ++ ((++ '/':bs) <$> ps))
+			runApt (("install":params) ++ ((++ '/':bs) <$> ps))
 				`changesFile` dpkgStatus
 	_ -> unsupportedOS'
   where
 	desc = unwords ("apt installed backport":ps)
-
--- | Minimal install of package, without recommends.
-installedMin :: [Package] -> Property DebianLike
-installedMin = installed' ["--no-install-recommends", "-y"]
 
 removed :: [Package] -> Property DebianLike
 removed ps = check (any (== IsInstalled) <$> getInstallStatus ps)
diff --git a/src/Propellor/Property/Borg.hs b/src/Propellor/Property/Borg.hs
--- a/src/Propellor/Property/Borg.hs
+++ b/src/Propellor/Property/Borg.hs
@@ -36,6 +36,9 @@
 	-- | Use to specify a ssh private key to use when accessing a
 	-- BorgRepo.
 	= UseSshKey FilePath
+	-- | Use to specify an environment variable to set when running
+	-- borg on a BorgRepo.
+	| UsesEnvVar (String, String)
 
 repoLoc :: BorgRepo -> String
 repoLoc (BorgRepo s) = s
@@ -53,6 +56,7 @@
 runBorgEnv (BorgRepoUsing os _) = map go os
   where
 	go (UseSshKey k) = ("BORG_RSH", "ssh -i " ++ k)
+	go (UsesEnvVar (k, v)) = (k, v)
 
 installed :: Property DebianLike
 installed = withOS desc $ \w o -> case o of
diff --git a/src/Propellor/Property/DiskImage.hs b/src/Propellor/Property/DiskImage.hs
--- a/src/Propellor/Property/DiskImage.hs
+++ b/src/Propellor/Property/DiskImage.hs
@@ -18,6 +18,7 @@
 	imageBuiltFrom,
 	imageExists,
 	GrubTarget(..),
+	noBootloader,
 ) where
 
 import Propellor.Base
@@ -228,6 +229,7 @@
 			ubootFlashKernelFinalized p
 		[FlashKernelInstalled, UbootInstalled p] -> 
 			ubootFlashKernelFinalized p
+		[NoBootloader] -> noBootloaderFinalized
 		_ -> unbootable "multiple bootloaders are installed; don't know which to use"
 
 -- | This property is automatically added to the chroot when building a
@@ -476,6 +478,15 @@
 ubootFlashKernelFinalized p img mnt loopdevs = 
 	ubootFinalized p img mnt loopdevs
 		`before` flashKernelFinalized img mnt loopdevs
+
+-- | Normally a boot loader is installed on a disk image. However,
+-- when the disk image will be booted by eg qemu booting the kernel and
+-- initrd, no boot loader is needed, and this property can be used.
+noBootloader :: Property (HasInfo + UnixLike)
+noBootloader = pureInfoProperty "no bootloader" [NoBootloader]
+
+noBootloaderFinalized :: Finalization
+noBootloaderFinalized _img _mnt _loopDevs = doNothing
 
 isChild :: FilePath -> Maybe MountPoint -> Bool
 isChild mntpt (Just d)
diff --git a/src/Propellor/Property/HostingProvider/CloudAtCost.hs b/src/Propellor/Property/HostingProvider/CloudAtCost.hs
deleted file mode 100644
--- a/src/Propellor/Property/HostingProvider/CloudAtCost.hs
+++ /dev/null
@@ -1,33 +0,0 @@
--- | Maintainer: currently unmaintained; your name here!
-
-module Propellor.Property.HostingProvider.CloudAtCost
-	{-# WARNING "This module does not have a maintainer. It might not work right anymore. If you use it, please consider becoming its maintainer." #-}
-	where
-
-import Propellor.Base
-import qualified Propellor.Property.Hostname as Hostname
-import qualified Propellor.Property.File as File
-import qualified Propellor.Property.User as User
-
--- Clean up a system as installed by cloudatcost.com
-decruft :: Property DebianLike
-decruft = propertyList "cloudatcost cleanup" $ props
-	& Hostname.sane
-	& grubbugfix
-	& nukecruft
-  where
-	grubbugfix :: Property DebianLike
-	grubbugfix = tightenTargets $ 
-		"/etc/default/grub" `File.containsLine` "GRUB_DISABLE_LINUX_UUID=true"
-			`describe` "worked around grub/lvm boot bug #743126"
-			`onChange` (cmdProperty "update-grub" [] `assume` MadeChange)
-			`onChange` (cmdProperty "update-initramfs" ["-u"] `assume` MadeChange)
-	nukecruft :: Property Linux
-	nukecruft = tightenTargets $
-		combineProperties "nuked cloudatcost cruft" $ props
-			& File.notPresent "/etc/rc.local"
-			& File.notPresent "/etc/init.d/S97-setup.sh"
-			& File.notPresent "/zang-debian.sh"
-			& File.notPresent "/bin/npasswd"
-			& User.nuked (User "user") User.YesReallyDeleteHome
-
diff --git a/src/Propellor/Property/Hostname.hs b/src/Propellor/Property/Hostname.hs
--- a/src/Propellor/Property/Hostname.hs
+++ b/src/Propellor/Property/Hostname.hs
@@ -14,8 +14,6 @@
 -- (However, when used inside a chroot, avoids setting the current hostname
 -- as that would impact the system outside the chroot.)
 --
--- Configures </etc/mailname> with the domain part of the hostname.
---
 -- </etc/hosts> is also configured, with an entry for 127.0.1.1, which is
 -- standard at least on Debian to set the FDQN.
 --
@@ -46,8 +44,6 @@
 	, check (not <$> inChroot) $
 		cmdProperty "hostname" [basehost]
 			`assume` NoChange
-	, "/etc/mailname" `File.hasContent`
-		[if null domain then hn else domain]
 	]
   where
 	desc = "hostname " ++ hn
@@ -84,6 +80,19 @@
 			| "domain " `isPrefixOf` l = False
 			| "search " `isPrefixOf` l = False
 			| otherwise = True
+
+-- Configures </etc/mailname> with the domain part of the hostname of the
+-- `Host` it's used in.
+mailname :: Property UnixLike
+mailname = mailname' extractDomain
+
+mailname' :: ExtractDomain -> Property UnixLike
+mailname' extractdomain = property' ("mailname set from hostname") $ \w ->
+	ensureProperty w . go =<< asks hostName
+  where
+	go mn = "/etc/mailname" `File.hasContent` [if null mn' then mn else mn']
+	  where
+	 	mn' = extractdomain mn
 
 -- | Function to extract the domain name from a HostName.
 type ExtractDomain = HostName -> String
diff --git a/src/Propellor/Property/Installer/Target.hs b/src/Propellor/Property/Installer/Target.hs
--- a/src/Propellor/Property/Installer/Target.hs
+++ b/src/Propellor/Property/Installer/Target.hs
@@ -24,6 +24,7 @@
 -- > seed ver = host "debian.local" $ props
 -- > 	& osDebian Unstable X86_64
 -- > 	& Hostname.sane
+-- >	& Hostname.mailname
 -- > 	& Apt.stdSourcesList
 -- > 	& Apt.installed ["linux-image-amd64"]
 -- > 	& Grub.installed PC
diff --git a/src/Propellor/Property/LetsEncrypt.hs b/src/Propellor/Property/LetsEncrypt.hs
--- a/src/Propellor/Property/LetsEncrypt.hs
+++ b/src/Propellor/Property/LetsEncrypt.hs
@@ -77,6 +77,9 @@
 		, "--text"
 		, "--noninteractive"
 		, "--keep-until-expiring"
+		-- The list of domains may be changed, adding more, so
+		-- always request expansion.
+		, "--expand"
 		] ++ map (\d -> "--domain="++d) alldomains
 
 	getstats = mapM statcertfiles alldomains
diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs
--- a/src/Propellor/Property/OS.hs
+++ b/src/Propellor/Property/OS.hs
@@ -58,6 +58,7 @@
 -- >        -- , oldOsRemoved (Confirmed "foo.example.com")
 -- >        ]
 -- > & Hostname.sane
+-- > & Hostname.mailname
 -- > & Apt.installed ["linux-image-amd64"]
 -- > & Apt.installed ["ssh"]
 -- > & User.hasSomePassword "root"
diff --git a/src/Propellor/Property/SiteSpecific/Branchable.hs b/src/Propellor/Property/SiteSpecific/Branchable.hs
--- a/src/Propellor/Property/SiteSpecific/Branchable.hs
+++ b/src/Propellor/Property/SiteSpecific/Branchable.hs
@@ -6,10 +6,10 @@
 import qualified Propellor.Property.User as User
 import qualified Propellor.Property.Ssh as Ssh
 import qualified Propellor.Property.Postfix as Postfix
-import qualified Propellor.Property.Gpg as Gpg
 import qualified Propellor.Property.Sudo as Sudo
 import qualified Propellor.Property.Borg as Borg
 import qualified Propellor.Property.Cron as Cron
+import Propellor.Property.SiteSpecific.JoeySites (rsyncNetBorgRepo)
 
 server :: [Host] -> Property (HasInfo + DebianLike)
 server hosts = propertyList "branchable server" $ props
@@ -39,35 +39,34 @@
 	& Postfix.installed
 	& Postfix.mainCf ("mailbox_command", "procmail -a \"$EXTENSION\"")
 	
-	& Borg.backup "/" (Borg.BorgRepo "joey@eubackup.kitenet.net:/home/joey/lib/backup/branchable/pell.borg") Cron.Daily
+	-- backup everything except the contents of sites, which are
+	-- backed up by ikiwiki-hosting.
+	& Borg.backup "/" (rsyncNetBorgRepo "pell.borg" []) Cron.Daily
 		[ "--exclude=/proc/*"
-		, "--exclude=/sys/*"
-		, "--exclude=/run/*"
-		, "--exclude=/tmp/*"
-		, "--exclude=/var/tmp/*"
-		, "--exclude=/var/backups/ikiwiki-hosting-web/*"
-		, "--exclude=/var/cache/*"
-		, "--exclude=/home/*/source/*"
-		, "--exclude=/home/*/public_html/*"
-		, "--exclude=/home/*/.git/*"
-		]
-		[ Borg.KeepDays 7
-		, Borg.KeepWeeks 5
-		, Borg.KeepMonths 12
-		, Borg.KeepYears 1
-		]
-	-- gpg key that can be used to decrypt the borg backup key
-	& Gpg.keyImported (Gpg.GpgKeyId obnamkey) (User "root")
+	 	, "--exclude=/sys/*"
+	 	, "--exclude=/run/*"
+	 	, "--exclude=/tmp/*"
+	 	, "--exclude=/var/tmp/*"
+	 	, "--exclude=/var/backups/ikiwiki-hosting-web/*"
+	 	, "--exclude=/var/cache/*"
+	 	, "--exclude=/home/*/source/*"
+	 	, "--exclude=/home/*/source.git/*"
+	 	, "--exclude=/home/*/public_html/*"
+	 	, "--exclude=/home/*/.git/*"
+	 	]
+	 	[ Borg.KeepDays 7
+	 	, Borg.KeepWeeks 5
+	 	, Borg.KeepMonths 12
+	 	, Borg.KeepYears 1
+	 	]
 	& Ssh.userKeys (User "root") (Context "branchable.com")
 		[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2PqTSupwncqeffNwZQXacdEWp7L+TxllIxH7WjfRMb3U74mQxWI0lwqLVW6Fox430DvhSqF1y5rJBvTHh4i49Tc9lZ7mwAxA6jNOP6bmdfteaKKYmUw5qwtJW0vISBFu28qBO11Nq3uJ1D3Oj6N+b3mM/0D3Y3NoGgF8+2dLdi81u9+l6AQ5Jsnozi2Ni/Osx2oVGZa+IQDO6gX8VEP4OrcJFNJe8qdnvItcGwoivhjbIfzaqNNvswKgGzhYLOAS5KT8HsjvIpYHWkyQ5QUX7W/lqGSbjP+6B8C3tkvm8VLXbmaD+aSkyCaYbuoXC2BoJdS7Jh8phKMwPJmdYVepn")
 		]
-	& Ssh.knownHost hosts "eubackup.kitenet.net" (User "root")
 	& Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
 
 	& adminuser "joey"
 	& adminuser "liw"
   where
-	obnamkey = "41E1A9B9"
 	adminuser u = propertyList ("admin user " ++ u) $ props
 		& User.accountFor (User u)
 		& User.hasSomePassword (User u)
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
@@ -151,7 +151,7 @@
 	manualinstall :: Architecture -> Property Linux
 	manualinstall arch = tightenTargets $ check (not <$> doesFileExist binstack) $
 		propertyList "stack installed from upstream tarball" $ props
-			& cmdProperty "wget" ["https://www.stackage.org/stack/linux-" ++ archname, "-O", tmptar]
+			& cmdProperty "wget" [url, "-O", tmptar]
 				`assume` MadeChange
 			& File.dirExists tmpdir
 			& cmdProperty "tar" ["xf", tmptar, "-C", tmpdir, "--strip-components=1"]
@@ -160,62 +160,29 @@
 				`assume` MadeChange
 			& cmdProperty "rm" ["-rf", tmpdir, tmptar]
 				`assume` MadeChange
+			& case arch of
+				ARMEL -> setupRevertableProperty $
+					"/lib/ld-linux-armhf.so.3"
+					`File.isSymlinkedTo`
+					File.LinkTarget "/lib/ld-linux.so.3"
+				_ -> doNothing
 	  where
-	  	-- See https://www.stackage.org/stack/ for the list of
-		-- binaries.
-		archname = case arch of
-			X86_32 -> "i386"
-			X86_64 -> "x86_64"
-			ARMHF -> "arm"
+		url = case arch of
+			X86_32 -> "https://www.stackage.org/stack/linux-i386"
+			X86_64 -> "https://www.stackage.org/stack/linux-x86_64"
+			ARMEL -> "https://github.com/commercialhaskell/stack/releases/download/v1.7.1/stack-1.7.1-linux-arm.tar.gz"
 			-- Probably not available.
-			a -> architectureToDebianArchString a
+			a -> "https://www.stackage.org/stack/linux-" ++ architectureToDebianArchString a
 	binstack = "/usr/bin/stack"
 	tmptar = "/root/stack.tar.gz"
 	tmpdir = "/root/stack"
 
-armAutoBuilder :: DebianSuite -> Architecture -> Flavor -> Property (HasInfo + Debian)
-armAutoBuilder suite arch flavor =
+armAutoBuilder :: (DebianSuite -> Architecture -> Flavor -> Property (HasInfo + Debian)) -> DebianSuite -> Architecture -> Flavor -> Property (HasInfo + Debian)
+armAutoBuilder baseautobuilder suite arch flavor =
 	propertyList "arm git-annex autobuilder" $ props
-		& standardAutoBuilder suite arch flavor
-		& buildDepsApt
+		& baseautobuilder suite arch flavor
 		-- Works around ghc crash with parallel builds on arm.
 		& (homedir </> ".cabal" </> "config")
-			`File.lacksLine` "jobs: $ncpus"
+			`File.containsLine` "jobs: 1"
 		-- Work around https://github.com/systemd/systemd/issues/7135
 		& Systemd.containerCfg "--system-call-filter=set_tls"
-
-androidAutoBuilderContainer :: Times -> TimeOut -> Systemd.Container
-androidAutoBuilderContainer crontimes timeout =
-	androidAutoBuilderContainer' "android-git-annex-builder"
-		(tree "android" Nothing) builddir crontimes timeout
-
--- Android is cross-built in a Debian i386 container, using the Android NDK.
-androidAutoBuilderContainer'
-	:: Systemd.MachineName
-	-> Property DebianLike
-	-> FilePath
-	-> Times
-	-> TimeOut
-	-> Systemd.Container
-androidAutoBuilderContainer' name setupgitannexdir gitannexdir crontimes timeout =
-	Systemd.container name $ \d -> bootstrap d $ props
-		& osDebian (Stable "jessie") X86_32
-		& Apt.stdSourcesList
-		& User.accountFor (User builduser)
-		& File.dirExists gitbuilderdir
-		& File.ownerGroup homedir (User builduser) (Group builduser)
-		& flagFile chrootsetup ("/chrootsetup")
-			`requires` setupgitannexdir
-		& haskellPkgsInstalled "android"
-		& Apt.unattendedUpgrades
-		& buildDepsNoHaskellLibs
-		& autobuilder "android" crontimes timeout
-  where
-	-- Use git-annex's android chroot setup script, which will install
-	-- ghc-android and the NDK, all build deps, etc, in the home
-	-- directory of the builder user.
-	chrootsetup = scriptProperty
-		[ "cd " ++ gitannexdir ++ " && ./standalone/android/buildchroot-inchroot"
-		]
-		`assume` MadeChange
-	bootstrap = Chroot.debootstrapped mempty
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,6 +16,7 @@
 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
@@ -23,6 +24,7 @@
 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.FileMode
 import Utility.Split
 
@@ -150,9 +152,8 @@
 			(User "root")
 			(Context "olduse.net")
 			(SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0F6L76SChMCIGmeyGhlFMUTgZ3BoTbATiOSs0A7KXQoI1LTE5ZtDzzUkrQRJVpJ640pfMR7cQZyBm8tv+kYIPp0238GrX43c1vgm0L78agDnBU7r2iNMyWIwhssK8O3ZAhp8Q4KCz1r8hP2nIiD0y1D1VWW8h4KWOS7I1XCEAjOTvFvEjTh6a9MyHrcIkv7teUUzTBRjNrsyijCFRk1+pEET54RueoOmEjQcWd/sK1tYRiMZjegRLBOus2wUWsUOvznJ2iniLONUTGAWRnEV+O7hLN6CD44osJ+wkZk8bPAumTS0zcSLckX1jpdHJicmAyeniWSd4FCqm1YE6/xDD")
-		`requires` Ssh.knownHost hosts "eubackup.kitenet.net" (User "root")
-	borgrepo = Borg.BorgRepoUsing [Borg.UseSshKey keyfile]
-		"joey@eubackup.kitenet.net:/home/joey/lib/backup/olduse.net/olduse.net.borg"
+		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
+	borgrepo = rsyncNetBorgRepo "olduse.net.borg" [Borg.UseSshKey keyfile]
 	keyfile = "/root/.ssh/olduse.net.key"
 
 oldUseNetShellBox :: Property DebianLike
@@ -177,22 +178,16 @@
 			`assume` MadeChange
 			`describe` "olduse.net built"
 
-kgbServer :: Property (HasInfo + Debian)
+kgbServer :: Property (HasInfo + DebianLike)
 kgbServer = propertyList desc $ props
-	& installed
+	& Apt.serviceInstalledRunning "kgb-bot"
+	& "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1"
+		`describe` "kgb bot enabled"
+		`onChange` Service.running "kgb-bot"
 	& File.hasPrivContent "/etc/kgb-bot/kgb.conf" anyContext
 		`onChange` Service.restarted "kgb-bot"
   where
 	desc = "kgb.kitenet.net setup"
-	installed :: Property Debian
-	installed = withOS desc $ \w o -> case o of
-		(Just (System (Debian _ Unstable) _)) ->
-			ensureProperty w $ propertyList desc $ props
-				& Apt.serviceInstalledRunning "kgb-bot"
-				& "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1"
-					`describe` "kgb bot enabled"
-					`onChange` Service.running "kgb-bot"
-		_ -> error "kgb server needs Debian unstable (for kgb-bot 1.31+)"
 
 -- git.kitenet.net and git.joeyh.name
 gitServer :: [Host] -> Property (HasInfo + DebianLike)
@@ -205,7 +200,7 @@
 			(User "root")
 			(Context "git.kitenet.net")
 			(SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLwUUkpkI9c2Wcnv/E4v9bJ7WcpiNkToltXfzRDd1F31AYrucfSMgzu3rtDpEL+wSnQLua/taJkWUWT/pyXOAh+90K6O/YeBZmY5CK01rYDz3kSTAtwHkMqednsRjdQS6NNJsuWc1reO8a4pKtsToJ3G9VAKufCkt2b8Nhqz0yLvLYwwU/mdI8DmfX6IgXhdy9njVEG/jsQnLFXY6QEfwKbIPs9O6qo4iFJg3defXX+zVMLsh3NE1P2i2VxMjxJEQdPdy9Z1sVpkiQM+mgJuylQQ5flPK8sxhO9r4uoK/JROkjPJNYoJMlsN+QlK04ABb7JV2JwhAL/Y8ypjQ13JdT")
-		`requires` Ssh.knownHost hosts "eubackup.kitenet.net" (User "root")
+		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
 	& Ssh.authorizedKeys (User "family") (Context "git.kitenet.net")
 	& User.accountFor (User "family")
 	& Apt.installed ["git", "rsync", "cgit"]
@@ -236,8 +231,7 @@
 	& Apache.modEnabled "cgi"
   where
 	sshkey = "/root/.ssh/git.kitenet.net.key"
-	borgrepo = Borg.BorgRepoUsing [Borg.UseSshKey sshkey]
-		"joey@eubackup.kitenet.net:/home/joey/lib/backup/git.kitenet.net/git.kitenet.net.borg"
+	borgrepo = rsyncNetBorgRepo "git.kitenet.net.borg" [Borg.UseSshKey sshkey]
 	website hn = Apache.httpsVirtualHost' hn "/srv/web/git.kitenet.net/" letos
 		[ Apache.iconDir
 		, "  <Directory /srv/web/git.kitenet.net/>"
@@ -339,12 +333,11 @@
 		& File.dirExists d
 		& File.ownerGroup d (User "joey") (Group "joey")
 
-downloads :: [Host] -> Property (HasInfo + DebianLike)
-downloads hosts = annexWebSite "/srv/git/downloads.git"
+downloads :: Property (HasInfo + DebianLike)
+downloads = annexWebSite "/srv/git/downloads.git"
 	"downloads.kitenet.net"
 	"840760dc-08f0-11e2-8c61-576b7e66acfd"
-	[("eubackup", "ssh://eubackup.kitenet.net/~/lib/downloads/")]
-	`requires` Ssh.knownHost hosts "eubackup.kitenet.net" (User "joey")
+	[]
 
 tmp :: Property (HasInfo + DebianLike)
 tmp = propertyList "tmp.joeyh.name" $ props
@@ -375,18 +368,6 @@
   where
 	conf = "/home/znc/.znc/configs/znc.conf"
 
-kiteShellBox :: Property DebianLike
-kiteShellBox = propertyList "kitenet.net shellinabox" $ props
-	& Apt.installed ["openssl", "shellinabox", "openssh-client"]
-	& File.hasContent "/etc/default/shellinabox"
-		[ "# Deployed by propellor"
-		, "SHELLINABOX_DAEMON_START=1"
-		, "SHELLINABOX_PORT=443"
-		, "SHELLINABOX_ARGS=\"--no-beep --service=/:SSH:kitenet.net\""
-		]
-		`onChange` Service.restarted "shellinabox"
-	& Service.running "shellinabox"
-
 githubBackup :: Property (HasInfo + DebianLike)
 githubBackup = propertyList "github-backup box" $ props
 	& Apt.installed ["github-backup", "moreutils"]
@@ -413,14 +394,6 @@
 	(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")
 
-backupsBackedupFrom :: [Host] -> HostName -> FilePath -> Property DebianLike
-backupsBackedupFrom hosts srchost destdir = Cron.niceJob desc
-	(Cron.Times "@reboot") (User "joey") "/" cmd
-	`requires` Ssh.knownHost hosts srchost (User "joey")
-  where
-	desc = "backups copied from " ++ srchost ++ " on boot"
-	cmd = "sleep 30m && rsync -az --bwlimit=300K --partial --delete " ++ srchost ++ ":lib/backup/ " ++ destdir </> srchost
-
 podcatcher :: Property DebianLike
 podcatcher = Cron.niceJob "podcatcher run hourly" (Cron.Times "55 * * * *")
 	(User "joey") "/home/joey/lib/sound/podcasts"
@@ -949,6 +922,7 @@
 			user (userGroup user)
 		`requires` File.dirExists (takeDirectory sshkeyfile)
 		`requires` Ssh.knownHost hosts "kitenet.net" user
+	&  File.hasPrivContentExposed "/etc/darksky-forecast-url" anyContext
   where
 	d = "/var/www/html/homepower"
 	sshkeyfile = d </> ".ssh/key"
@@ -1042,15 +1016,18 @@
 	-- rsync server command to be updated too.
 	rsynccommand = "rsync -e 'ssh -i" ++ sshkeyfile ++ "' -avz rrds/ joey@kitenet.net:/srv/web/homepower.joeyh.name/rrds/"
 
--- My home router, running hostapd and dnsmasq for wlan0,
+homerouterWifiInterface :: String
+homerouterWifiInterface = "wlan0" -- "wlx7cdd90400448" is a wifi dongle
+
+-- My home router, running hostapd and dnsmasq,
 -- with eth0 connected to a satellite modem, and a fallback ppp connection.
 homeRouter :: Property (HasInfo + DebianLike)
 homeRouter = propertyList "home router" $ props
-	& Network.static "wlan0" (IPv4 "10.1.1.1") Nothing
+	& Network.static homerouterWifiInterface (IPv4 "10.1.1.1") Nothing
 		`requires` Network.cleanInterfacesFile
 	& Apt.installed ["hostapd"]
 	& File.hasContent "/etc/hostapd/hostapd.conf"
-			[ "interface=wlan0"
+			[ "interface=" ++ homerouterWifiInterface
 			, "ssid=house"
 			, "hw_mode=g"
 			, "channel=8"
@@ -1059,6 +1036,7 @@
 		`requires` File.hasContent "/etc/default/hostapd"
 			[ "DAEMON_CONF=/etc/hostapd/hostapd.conf" ]
 		`onChange` Service.running "hostapd"
+	& Systemd.enabled "hostapd"
 	& File.hasContent "/etc/resolv.conf"
 		[ "domain kitenet.net"
 		, "search kitenet.net"
@@ -1069,7 +1047,8 @@
 	& File.hasContent "/etc/dnsmasq.conf"
 		[ "domain-needed"
 		, "bogus-priv"
-		, "interface=wlan0"
+		, "interface=" ++ homerouterWifiInterface
+		, "interface=eth0"
 		, "domain=kitenet.net"
 		-- lease time is short because the homepower
 		-- controller wants to know when clients disconnect
@@ -1077,10 +1056,13 @@
 		, "no-hosts"
 		, "address=/honeybee.kitenet.net/10.1.1.1"
 		, "address=/house.kitenet.net/10.1.1.1"
+		, "dhcp-host=0c:98:38:80:6a:f9,10.1.1.134,android-kodama"
 		]
 		`onChange` Service.restarted "dnsmasq"
-	& ipmasq "wlan0"
-	& Apt.serviceInstalledRunning "netplug"
+	& ipmasq homerouterWifiInterface
+	-- Used to bring down eth0 when satellite is off, which causes ppp
+	-- to start, but I am not using this currently.
+	& Apt.removed ["netplug"]
 	& Network.static' "eth0" (IPv4 "192.168.1.100")
 		(Just (Network.Gateway (IPv4 "192.168.1.1")))
 		-- When satellite is down, fall back to dialup
@@ -1101,7 +1083,7 @@
 			]
 		`before` File.hasPrivContent "/etc/ppp/pap-secrets" (Context "joeyh@arczip.com")
 
--- | Enable IP masqerading, on whatever other interfaces come up than the
+-- | Enable IP masqerading, on whatever other interfaces come up, besides the
 -- provided intif.
 ipmasq :: String -> Property DebianLike
 ipmasq intif = File.hasContent ifupscript
@@ -1117,6 +1099,7 @@
 	, "echo 1 > /proc/sys/net/ipv4/ip_forward"
 	]
 	`before` scriptmode ifupscript
+	`before` File.dirExists (takeDirectory pppupscript)
 	`before` File.hasContent pppupscript
 		[ "#!/bin/sh"
 		, "IFACE=$PPP_IFACE " ++ ifupscript
@@ -1140,7 +1123,7 @@
 	, "mpc", "mpd", "ncmpc", "sonata", "mpdtoys"
 	, "bsdgames", "nethack-console"
 	, "xmonad", "libghc-xmonad-dev", "libghc-xmonad-contrib-dev"
-	, "ttf-bitstream-vera"
+	, "ttf-bitstream-vera", "fonts-symbola", "fonts-noto-color-emoji"
 	, "mairix", "offlineimap", "mutt", "slrn"
 	, "mtr", "nmap", "whois", "wireshark", "tcpdump", "iftop"
 	, "pmount", "tree", "pv"
@@ -1179,16 +1162,20 @@
 	]
 
 cubieTruckOneWire :: Property DebianLike
-cubieTruckOneWire = 
-	File.hasContent "/etc/easy-peasy-devicetree-squeezy/my.dts" mydts
-		`onChange` utilitysetup
-		`requires` utilityinstalled
+cubieTruckOneWire = utilitysetup
+	`requires` dtsinstalled
+	`requires` utilityinstalled
   where
+	dtsinstalled = File.hasContent "/etc/easy-peasy-devicetree-squeezy/my.dts" mydts
+		`requires` File.dirExists "/etc/easy-peasy-devicetree-squeezy"
 	utilityinstalled = Git.cloned (User "root") "https://git.joeyh.name/git/easy-peasy-devicetree-squeezy.git" "/usr/local/easy-peasy-devicetree-squeezy" Nothing
 		`onChange` File.isSymlinkedTo "/usr/local/bin/easy-peasy-devicetree-squeezy" (File.LinkTarget "/usr/local/easy-peasy-devicetree-squeezy/easy-peasy-devicetree-squeezy")
-	utilitysetup = cmdProperty "easy-peasy-devicetree-squeezy"
-		["--debian", "sun7i-a20-cubietruck"]
-		`assume` MadeChange
+		`requires` Apt.installed ["pv", "device-tree-compiler", "cpp", "linux-source"]
+	utilitysetup = check (not <$> doesFileExist dtb) $ 
+		cmdProperty "easy-peasy-devicetree-squeezy"
+			["--debian", "sun7i-a20-cubietruck"]
+			`assume` MadeChange
+	dtb = "/etc/flash-kernel/dtbs/sun7i-a20-cubietruck.dtb"
 	mydts =
 		[ "/* Device tree addition enabling onewire sensors on CubieTruck GPIO pin PC21 */"
 		, "#include <dt-bindings/gpio/gpio.h>"
@@ -1209,3 +1196,102 @@
 		, "\t};"
 		, "};"
 		]
+
+-- 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}==\"0409\", ATTR{idProduct}==\"005a\", MODE=\"0666\""
+		]
+	& autoMountDrive "archive-10" (USBHubPort 1) (Just "archive-older")
+	& autoMountDrive "archive-11" (USBHubPort 2) (Just "archive-old")
+	& autoMountDrive "archive-12" (USBHubPort 3) (Just "archive")
+	& autoMountDrive "passport" (USBHubPort 4) Nothing
+	& Apt.installed ["git-annex", "borgbackup"]
+
+newtype USBHubPort = USBHubPort Int
+
+-- 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.
+autoMountDrive :: Mount.Label -> USBHubPort -> Maybe FilePath -> Property DebianLike
+autoMountDrive label (USBHubPort port) 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
+		, "Requires=" ++ hub
+		, "After=" ++ hub
+		, "[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/" ++ hub)
+		[ "[Unit]"
+		, "Description=Startech usb hub port " ++ show port
+		, "PartOf=" ++ mount
+		, "[Service]"
+		, "Type=oneshot"
+		, "RemainAfterExit=true"
+		, "ExecStart=/usr/sbin/uhubctl -a on -p " ++ show port
+		, "ExecStop=/bin/sh -c 'uhubctl -a off -p " ++ show port ++
+			-- 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
+	& 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
+	hub = "startech-hub-port-" ++ show port ++ ".service"
+	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
+	-- rsync.net has a newer borg here
+	os' = Borg.UsesEnvVar ("BORG_REMOTE_PATH", "borg1") : os
diff --git a/src/Propellor/Property/Sudo.hs b/src/Propellor/Property/Sudo.hs
--- a/src/Propellor/Property/Sudo.hs
+++ b/src/Propellor/Property/Sudo.hs
@@ -7,37 +7,65 @@
 import qualified Propellor.Property.Apt as Apt
 import Propellor.Property.User
 
--- | Allows a user to sudo. If the user has a password, sudo is configured
--- to require it. If not, NOPASSWORD is enabled for the user.
+-- | Allows a user to run any command with sudo.
+-- If the user has a password, sudo is configured to require it.
+-- If not, NOPASSWORD is enabled for the user.
+--
+-- Writes to the file /etc/sudoers.d/000users rather than the main sudoers
+-- file. This file should come before other include files that may eg,
+-- allow running more specific commands without a password, since sudo
+-- uses the last matching configuration line.
+--
+-- If the main sudoers file contains a conflicting line for
+-- the user for ALL commands, the line will be removed.
+--
+-- Also ensures that the main sudoers file includes /etc/sudoers.d/
 enabledFor :: User -> RevertableProperty DebianLike DebianLike
 enabledFor user@(User u) = setup `requires` Apt.installed ["sudo"] <!> cleanup
   where
 	setup :: Property UnixLike
 	setup = property' desc $ \w -> do
 		locked <- liftIO $ isLockedPassword user
-		ensureProperty w $
-			fileProperty desc
+		ensureProperty w $ combineProperties desc $ props
+			& containsLine sudoers "#includedir /etc/sudoers.d"
+			& fileProperty desc
 				(modify locked . filter (wanted locked))
-				sudoers
+				dfile
+			& removeconflicting sudoers
 	  where
 		desc = u ++ " is sudoer"
 	
 	cleanup :: Property DebianLike
-	cleanup = tightenTargets $ 
-		fileProperty desc (filter notuserline) sudoers
+	cleanup = tightenTargets $ combineProperties desc $ props
+		& removeconflicting sudoers
+		& removeconflicting dfile
 	  where
 		desc = u ++ " is not sudoer"
 	
+	removeconflicting = fileProperty "remove conflicting" (filter notuserline)
+
 	sudoers = "/etc/sudoers"
+	dfile = "/etc/sudoers.d/000users"
 	sudobaseline = u ++ " ALL=(ALL:ALL)"
 	notuserline l = not (sudobaseline `isPrefixOf` l)
 	sudoline True = sudobaseline ++ " NOPASSWD:ALL"
 	sudoline False = sudobaseline ++ " ALL"
 	wanted locked l
-		-- TOOD: Full sudoers file format parse.. 
 		| notuserline l = True
 		| "NOPASSWD" `isInfixOf` l = locked
 		| otherwise = True
 	modify locked ls
 		| sudoline locked `elem` ls = ls
 		| otherwise = ls ++ [sudoline locked]
+
+-- | Sets up a file in /etc/sudoers.d/, which /etc/sudoers includes,
+-- with the specified content.
+--
+-- The FilePath can be relative to that directory.
+sudoersDFile :: FilePath -> [Line] -> RevertableProperty DebianLike Linux
+sudoersDFile dfile content = setup `requires` Apt.installed ["sudo"] <!> cleanup
+  where
+	f = "/etc/sudoers.d" </> dfile
+	-- sudoers.d files should not be world readable
+	setup = hasContentProtected f content
+	cleanup = tightenTargets $ notPresent f
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
@@ -13,6 +13,7 @@
 	networkd,
 	journald,
 	logind,
+	escapePath,
 	-- * Configuration
 	installed,
 	Option,
@@ -58,7 +59,9 @@
 import Utility.Split
 
 import Data.List
+import Data.Char
 import qualified Data.Map as M
+import Text.Printf
 
 type ServiceName = String
 
@@ -459,3 +462,15 @@
 -- | Read-only mind mount.
 bindRo :: Bindable p => p -> RevertableProperty (HasInfo + Linux) (HasInfo + Linux)
 bindRo p = containerCfg $ "--bind-ro=" ++ toBind p
+
+-- | Escapes a path for inclusion in a systemd unit name,
+-- the same as systemd-escape does.
+escapePath :: FilePath -> String
+escapePath = concatMap escape 
+	. dropWhile (== '/')
+	. reverse . dropWhile (== '/') . reverse
+  where
+	escape '/' = "-"
+	escape c
+		| ((isAscii c && isAlphaNum c) || c == '_') = [c]
+		| otherwise = '\\' : 'x' : printf "%x" c
diff --git a/src/Propellor/Types/Bootloader.hs b/src/Propellor/Types/Bootloader.hs
--- a/src/Propellor/Types/Bootloader.hs
+++ b/src/Propellor/Types/Bootloader.hs
@@ -10,6 +10,7 @@
 	= GrubInstalled GrubTarget
 	| FlashKernelInstalled
 	| UbootInstalled (FilePath -> FilePath -> Property Linux)
+	| NoBootloader
 	deriving (Typeable)
 
 -- | Platforms that grub can boot.
@@ -19,6 +20,7 @@
 	show (GrubInstalled _) = "GrubInstalled"
 	show FlashKernelInstalled = "FlashKernelInstalled"
 	show (UbootInstalled _) = "UbootInstalled"
+	show NoBootloader = "NoBootloader"
 
 instance IsInfo [BootloaderInstalled] where
 	propagateInfo _ = PropagateInfo False
