diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,22 @@
+propellor (5.3.0) unstable; urgency=medium
+
+  * Avoid bogus warning about new upstream version when /usr/bin/propellor
+    is run on a Debian system, but ~/.propellor was not cloned from the
+    Debian git bundle.
+  * Parted: Allow partitions to have no filesystem, for eg, GPT BIOS boot
+    partitions. (API change)
+  * Added rawPartition to PartSpec, for specifying partitions with no
+    filesystem.
+  * Added BiosGrubFlag to PartFlag.
+  * Add HasCallStack constraint to pickOS and unsupportedOS, so the
+    call stack includes the caller.
+  * Run su with --login, to avoid inheriting some problematic environment
+    variables, such as TMP, from the caller.
+  * Grub: Added properties to configure /etc/default/grub.
+  * Laptop: New module, starting with powertopAutoTuneOnBoot.
+
+ -- Joey Hess <id@joeyh.name>  Thu, 01 Feb 2018 12:27:01 -0400
+
 propellor (5.2.0) unstable; urgency=medium
 
   [ Joey Hess ]
diff --git a/config.hs b/config.hs
--- a/config.hs
+++ b/config.hs
@@ -1,29 +1,658 @@
--- This is the main configuration file for Propellor, and is used to build
--- the propellor program.
+-- This is the live config file used by propellor's author.
+-- https://propellor.branchable.com/
+module Main where
 
 import Propellor
+import Propellor.Property.Scheduled
+import Propellor.Property.DiskImage
+import Propellor.Property.Chroot
+import Propellor.Property.Machine
+import Propellor.Property.Bootstrap
 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
 import qualified Propellor.Property.User as User
+import qualified Propellor.Property.Hostname as Hostname
+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
+import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
+import qualified Propellor.Property.Locale as Locale
+import qualified Propellor.Property.Grub as Grub
+import qualified Propellor.Property.FlashKernel as FlashKernel
+import qualified Propellor.Property.Borg as Borg
+import qualified Propellor.Property.Gpg as Gpg
+import qualified Propellor.Property.Systemd as Systemd
+import qualified Propellor.Property.Journald as Journald
+import qualified Propellor.Property.Fail2Ban as Fail2Ban
+import qualified Propellor.Property.Laptop as Laptop
+import qualified Propellor.Property.OS as OS
+import qualified Propellor.Property.HostingProvider.CloudAtCost as CloudAtCost
+import qualified Propellor.Property.HostingProvider.Linode as Linode
+import qualified Propellor.Property.HostingProvider.DigitalOcean as DigitalOcean
+import qualified Propellor.Property.SiteSpecific.GitHome as GitHome
+import qualified Propellor.Property.SiteSpecific.GitAnnexBuilder as GitAnnexBuilder
+import qualified Propellor.Property.SiteSpecific.Branchable as Branchable
+import qualified Propellor.Property.SiteSpecific.JoeySites as JoeySites
 
-main :: IO ()
-main = defaultMain hosts
+main :: IO ()           --     _         ______`|                       ,-.__
+main = defaultMain hosts --  /   \___-=O`/|O`/__|                      (____.'
+  {- Propellor            -- \          / | /    )          _.-"-._
+     Deployed -}          --  `/-==__ _/__|/__=-|          (       \_
+hosts :: [Host]          --   *             \ | |           '--------'
+hosts =                 --                  (o)  `
+	[ darkstar
+	, gnu
+	, dragon
+	, clam
+	, orca
+	, baleen
+	, honeybee
+	, kite
+	, elephant
+	, beaver
+	, mouse
+	, peregrine
+	, pell
+	, keysafe
+	] ++ monsters
 
--- The hosts propellor knows about.
-hosts :: [Host]
-hosts =
-	[ mybox
-	]
+testvm :: Host
+testvm = host "testvm.kitenet.net" $ props
+	& osDebian Unstable X86_64
+	& OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.net")
+	 	`onChange` postinstall
+	& Hostname.sane
+	& Hostname.searchDomain
+	& Apt.installed ["linux-image-amd64"]
+	& Apt.installed ["ssh"]
+	& User.hasPassword (User "root")
+  where
+	postinstall :: Property (HasInfo + DebianLike)
+	postinstall = propertyList "fixing up after clean install" $ props
+		& OS.preserveRootSshAuthorized
+		& OS.preserveResolvConf
+		& Apt.update
+		& Grub.boots "/dev/sda"
+			`requires` Grub.installed Grub.PC
 
--- An example host.
-mybox :: Host
-mybox = host "mybox.example.com" $ props
+darkstar :: Host
+darkstar = host "darkstar.kitenet.net" $ props
 	& osDebian Unstable X86_64
-	& Apt.stdSourcesList
+	& ipv6 "2001:4830:1600:187::2"
+	& Hostname.sane
+	& Apt.serviceInstalledRunning "swapspace"
+	& Laptop.powertopAutoTuneOnBoot
+	& Grub.cmdline_Linux_default "i915.enable_psr=1"
+	! Grub.cmdline_Linux_default "quiet"
+
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+	-- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
+	--	"/usr/bin/timeout 45m /home/joey/bin/goodmorning"
+	& 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")
+		]
+	-- & imageBuiltFor honeybee
+	--	(RawDiskImage "/srv/honeybee.img")
+	--	(Debootstrapped mempty)
+
+gnu :: Host
+gnu = host "gnu.kitenet.net" $ props
+	& Postfix.satellite
+
+dragon :: Host
+dragon = host "dragon.kitenet.net" $ props
+	& ipv6 "2001:4830:1600:187::2"
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+
+clam :: Host
+clam = host "clam.kitenet.net" $ props
+	& standardSystem Unstable X86_64
+		["Unreliable server. Anything here may be lost at any time!" ]
+	& ipv4 "45.62.211.94"
+
+	& CloudAtCost.decruft
+	& Ssh.hostKeys hostContext
+		[ (SshDsa, "ssh-dss AAAAB3NzaC1kc3MAAACBAI3WUq0RaigLlcUivgNG4sXpso2ORZkMvfqKz6zkc60L6dpxvWDNmZVEH8hEjxRSYG07NehcuOgQqeyFnS++xw1hdeGjf37JqCUH49i02lra3Zxv8oPpRxyeqe5MmuzUJhlWvBdlc3O/nqZ4bTUfnxMzSYWyy6++s/BpSHttZplNAAAAFQC1DE0vzgVeNAv9smHLObQWZFe2VQAAAIBECtpJry3GC8NVTFsTHDGWksluoFPIbKiZUFFztZGdM0AO2VwAbiJ6Au6M3VddGFANgTlni6d2/9yS919zO90TaFoIjywZeXhxE2CSuRfU7sx2hqDBk73jlycem/ER0sanFhzpHVpwmLfWneTXImWyq37vhAxatJANOtbj81vQ3AAAAIBV3lcyTT9xWg1Q4vERJbvyF8mCliwZmnIPa7ohveKkxlcgUk5d6dnaqFfjVaiXBPN3Qd08WXoQ/a9k3chBPT9nW2vWgzzM8l36j2MbHLmaxGwevAc9+vx4MXqvnGHzd2ex950mC33ct3j0fzMZlO6vqEsgD4CYmiASxhfefj+JCQ==")
+		, (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJybAjUPUWIhvVMmer8K5ZgdfI54DM6vc8Mzw+5KmVKL0TwkvzbR1HAB4heyMGtN1F8YzkWhsI3/Txh+MQUJ+i4u8SvSYc6D1q3j3ZyCi06wZ3DJS25tZrOM/thOOA1DFA4Hhb0uI/1Kg8PguNNNSMXn8F7q3F6cFQizYgszs6z6ktiST/BTC+IXWovhcnn2vQXXU8FTcTsqBFqA5dEjZbp1WDzqp3km84ZyXGmoVlpqzXeMvlkWTIshYiQjXIwPOkALzlGYjp1lw1OaxPVI1IGFcgCbIWQQWoCReb+genX2VaR+odAYXjaOdRx0lQj7UCPTBCpqMyzBMLtT5Yiaqh")
+		, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPhfvcOuw0Yt+MnsFc4TI2gWkKi62Eajxz+TgbHMO/uRTYF8c5V8fOI3o+J/3m5+lT0S5o8j8a7xIC3COvi+AVw=")
+		]
 	& Apt.unattendedUpgrades
-	& Apt.installed ["etckeeper"]
+	& Systemd.persistentJournal
+	& Journald.systemMaxUse "50MiB"
+	& Apt.serviceInstalledRunning "swapspace"
+
+	& Tor.isRelay
+	& Tor.named "kite1"
+	& Tor.bandwidthRate (Tor.PerMonth "400 GB")
+
+	& Systemd.nspawned oldusenetShellBox
+
+	& JoeySites.scrollBox
+	& alias "scroll.joeyh.name"
+	& alias "us.scroll.joeyh.name"
+
+baleen :: Host
+baleen = host "baleen.kitenet.net" $ props
+	& standardSystem Unstable X86_64 [ "New git-annex build box." ]
+
+	-- Not on public network; ssh access via bounce host.
+	& ipv4 "138.38.77.40"
+	
+	-- The root filesystem content may be lost if the VM is resized.
+	-- /dev/vdb contains persistent storage.
+	& Fstab.mounted "auto" "/dev/vdb" "/var/lib/container" mempty
+	
+	& Apt.unattendedUpgrades
+	& Postfix.satellite
+	& Apt.serviceInstalledRunning "ntp"
+	& Systemd.persistentJournal
+
+orca :: Host
+orca = host "orca.kitenet.net" $ props
+	& standardSystem Unstable X86_64 [ "Main git-annex build box." ]
+	& ipv4 "138.38.108.179"
+
+	& Apt.unattendedUpgrades
+	& Postfix.satellite
+	& Apt.serviceInstalledRunning "ntp"
+	& Systemd.persistentJournal
+
+	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
+		GitAnnexBuilder.standardAutoBuilder
+		Unstable X86_64 Nothing (Cron.Times "15 * * * *") "2h")
+	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
+		GitAnnexBuilder.standardAutoBuilder
+		Unstable X86_32 Nothing (Cron.Times "30 * * * *") "2h")
+	& 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")
+
+honeybee :: Host
+honeybee = host "honeybee.kitenet.net" $ props
+	& standardSystem Testing ARMHF
+		[ "Home router and arm git-annex build box." ]
+	
+	& cubietech_Cubietruck
+	& hasPartition
+		( partition EXT4
+			`mountedAt` "/"
+			`setSize` MegaBytes 8000
+		)
+	& File.hasPrivContentExposed "/etc/flash-kernel/dtbs/sun7i-a20-cubietruck.dtb"
+		(Context "cubietruck gpio")
+		`onChange` FlashKernel.flashKernel
+	
+	& 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
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIS/hDYq1MAxfOBf49htym3BOYlx4Gk9SDpiHjv7u6IC")
+		]
+
+	& JoeySites.homePowerMonitor
+		(User "joey")
+		hosts
+		(Context "homepower.joeyh.name")
+		(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMAmVYddg/RgCbIj+cLcEiddeFXaYFnbEJ3uGj9G/EyV joey@honeybee")
+	& JoeySites.homeRouter
+	& Apt.installed ["mtr-tiny", "iftop", "screen"]
+	& Postfix.satellite
+
+	& check (not <$> inChroot) (setupRevertableProperty autobuilder)
+	-- In case compiler needs more than available ram
+	& Apt.serviceInstalledRunning "swapspace"
+  where
+	autobuilder = Systemd.nspawned $ GitAnnexBuilder.autoBuilderContainer
+		GitAnnexBuilder.armAutoBuilder
+		Unstable ARMEL Nothing (Cron.Times "15 10 * * *") "10h"
+
+-- This is not a complete description of kite, since it's a
+-- multiuser system with eg, user passwords that are not deployed
+-- with propellor.
+kite :: Host
+kite = host "kite.kitenet.net" $ props
+	& standardSystemUnhardened Testing X86_64 [ "Welcome to kite!" ]
+	& ipv4 "66.228.36.95"
+	& ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
+	& alias "kitenet.net"
+	& alias "wren.kitenet.net" -- temporary
+	& 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")
+		, (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA2QAJEuvbTmaN9ex9i9bjPhMGj+PHUYq2keIiaIImJ+8mo+yKSaGUxebG4tpuDPx6KZjdycyJt74IXfn1voGUrfzwaEY9NkqOP3v6OWTC3QeUGqDCeJ2ipslbEd9Ep9XBp+/ldDQm60D0XsIZdmDeN6MrHSbKF4fXv1bqpUoUILk=")
+		, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLF+dzqBJZix+CWUkAd3Bd3cofFCKwHMNRIfwx1G7dL4XFe6fMKxmrNetQcodo2edyufwoPmCPr3NmnwON9vyh0=")
+		, (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFZftKMnH/zH29BHMKbcBO4QsgTrstYFVhbrzrlRzBO3")
+		]
+
+	& Network.preserveStatic "eth0" `requires` Network.cleanInterfacesFile
+	& Apt.installed ["linux-image-amd64"]
+	& Linode.serialGrub
+	& Linode.mlocateEnabled
+	& Apt.unattendedUpgrades
+	& Systemd.installed
+	& Systemd.persistentJournal
+	& Journald.systemMaxUse "500MiB"
+	& Ssh.passwordAuthentication True
+	& Fail2Ban.installed -- since ssh password authentication is allowed
+	-- Allow ssh -R to forward ports via kite
+	& Ssh.setSshdConfig "GatewayPorts" "clientspecified"
+	& 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
+		[ "--exclude=/proc/*"
+		, "--exclude=/sys/*"
+		, "--exclude=/run/*"
+		, "--exclude=/tmp/*"
+		, "--exclude=/var/tmp/*"
+		, "--exclude=/var/cache/*"
+		, "--exclude=/home/joey/lib"
+		-- These directories are backed up and restored separately.
+		, "--exclude=/srv/git"
+		, "--exclude=/var/spool/oldusenet"
+		]
+		[ Borg.KeepDays 7
+		, Borg.KeepWeeks 4
+		, Borg.KeepMonths 6
+		]
+		`requires` Ssh.knownHost hosts "eubackup.kitenet.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")
+			]
+
+	& alias "smtp.kitenet.net"
+	& alias "imap.kitenet.net"
+	& alias "pop.kitenet.net"
+	& alias "mail.kitenet.net"
+	& JoeySites.kiteMailServer
+
+	& JoeySites.legacyWebSites
+	& File.ownerGroup "/srv/web" (User "joey") (Group "joey")
+	& Apt.installed ["analog"]
+
+	& alias "git.kitenet.net"
+	& alias "git.joeyh.name"
+	& JoeySites.gitServer hosts
+
+	& JoeySites.downloads hosts
+	& 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"
+		, "rss2email", "archivemail"
+		, "devscripts"
+		-- Some users have zsh as their login shell.
+		, "zsh"
+		]
+
+	& alias "nntp.olduse.net"
+	& JoeySites.oldUseNetServer hosts
+
+	& alias "ns4.kitenet.net"
+	& myDnsPrimary True "kitenet.net"
+		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
+		, (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
+		, (RelDomain "peregrine-onion", CNAME $ AbsDomain "ahw47zqw6qszoufl.onion")
+		]
+	& myDnsPrimary True "joeyh.name" []
+	& myDnsPrimary True "ikiwiki.info" []
+	& myDnsPrimary True "olduse.net"
+		[ (RelDomain "article", CNAME $ AbsDomain "virgil.koldfront.dk")
+		]
+	& alias "ns4.branchable.com"
+	& branchableSecondary
+	& Dns.secondaryFor ["animx"] hosts "animx.eu.org"
+	-- Use its own name server (amoung other things this avoids
+	-- spamassassin URIBL_BLOCKED.
+	& "/etc/resolv.conf" `File.hasContent`
+		[ "nameserver 127.0.0.1"
+		, "domain kitenet.net"
+		, "search kitenet.net"
+		]
+	& alias "debug-me.joeyh.name"
+	-- debug-me installed manually until package is available
+	& Systemd.enabled "debug-me"
+
+	-- testing
+	& 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/"
+
+mouse :: Host
+mouse = host "mouse.kitenet.net" $ props
+	& ipv4 "67.223.19.96"
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+peregrine :: Host
+peregrine = host "peregrine.kitenet.net" $ props
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+-- Branchable is not completely deployed with propellor yet.
+pell :: Host
+pell = host "pell.branchable.com" $ props
+	& alias "branchable.com"
+	& ipv4 "66.228.46.55"
+	& ipv6 "2600:3c03::f03c:91ff:fedf:c0e5"
+
+	-- All the websites I host at branchable that don't use
+	-- branchable.com dns.
+	& alias "olduse.net"
+	& alias "www.olduse.net"
+	& alias "www.kitenet.net"
+	& alias "joeyh.name"
+	& alias "www.joeyh.name"
+	& alias "campaign.joeyh.name"
+	& alias "ikiwiki.info"
+	& alias "www.ikiwiki.info"
+	& alias "git.ikiwiki.info"
+	& alias "l10n.ikiwiki.info"
+	& alias "dist-bugs.kitenet.net"
+	& alias "family.kitenet.net"
+
+	& osDebian (Stable "stretch") X86_64
+	& Apt.installed ["linux-image-686-pae"]
+	& Apt.unattendedUpgrades
+	& Branchable.server hosts
+	& Linode.serialGrub
+
+-- See https://joeyh.name/code/keysafe/servers/ for requirements.
+keysafe :: Host
+keysafe = host "keysafe.joeyh.name" $ props
+	& ipv4 "139.59.17.168"
+	& Hostname.sane
+	& osDebian (Stable "stretch") X86_64
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& DigitalOcean.distroKernel
+	-- This is a 500 mb VM, so need more ram to build propellor.
+	& Apt.serviceInstalledRunning "swapspace"
+	& Cron.runPropellor (Cron.Times "30 * * * *")
+	& Apt.installed ["etckeeper", "sudo"]
+	& Apt.removed ["nfs-common", "exim4", "exim4-base", "exim4-daemon-light", "rsyslog", "acpid", "rpcbind", "at"]
+
 	& User.hasSomePassword (User "root")
-	& File.dirExists "/var/www"
+	& User.accountFor (User "joey")
+	& User.hasSomePassword (User "joey")
+	& Sudo.enabledFor (User "joey")
+
+	& Ssh.installed
+	& Ssh.randomHostKeys
+	& User "root" `Ssh.authorizedKeysFrom` (User "joey", darkstar)
+	& User "joey" `Ssh.authorizedKeysFrom` (User "joey", darkstar)
+	& Ssh.noPasswords
+
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "keysafe" (Port 4242)
+		`requires` Tor.hiddenServiceData "keysafe" hostContext
+	& Tor.bandwidthRate (Tor.PerMonth "750 GB")
+
+	-- keysafe installed manually until package is available
+	& Systemd.enabled "keysafe"
+
+	& Gpg.keyImported (Gpg.GpgKeyId "CECE11AE") (User "root")
+	& Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
+	& Ssh.userKeys (User "root")
+		(Context "keysafe.joeyh.name")
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEx8bK9ZbXVEgEvxQeXLjnr9cGa/QvoB459aglP529My root@keysafe")
+		]
+	-- Note that this is not an incremental backup; it uploads the
+	-- whole content every time. So, only run weekly.
+	& Cron.niceJob "keysafe backup" Cron.Weekly (User "root") "/" backupcmd
+		`requires` Apt.installed ["rsync"]
+  where
+	datadir = "/var/lib/keysafe"
+	backupdir = "/var/backups/keysafe"
+	rsyncnetbackup = "2318@usw-s002.rsync.net:keysafe"
+	backupcmd = unwords
+		[ "keysafe --store-directory", datadir, "--backup-server", backupdir
+		, "&& rsync -a --delete --max-delete 3 ",  backupdir , rsyncnetbackup
+		]
+
+       --'                        __|II|      ,.
+     ----                      __|II|II|__   (  \_,/\
+--'-------'\o/-'-.-'-.-'-.- __|II|II|II|II|___/   __/ -'-.-'-.-'-.-'-.-'-.-'-
+-------------------------- |   [Containers]      / --------------------------
+-------------------------- :                    / ---------------------------
+--------------------------- \____, 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
+	& standardContainer (Stable "stretch")
+	& alias hn
+	& Git.cloned (User "root") "git://kitenet-net.branchable.com/" "/var/www/html"
+		(Just "remotes/origin/old-kitenet.net")
+	& Apache.installed
+	& Apache.listenPorts [p]
+	& Apache.virtualHost hn p "/var/www/html"
+	& Apache.siteDisabled "000-default"
+  where
+	p = Port 1994
+	hn = "ancient.kitenet.net"
+
+oldusenetShellBox :: Systemd.Container
+oldusenetShellBox = Systemd.debContainer "oldusenet-shellbox" $ props
+	& standardContainer (Stable "stretch")
+	& 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.
+standardSystem :: DebianSuite -> Architecture -> Motd -> Property (HasInfo + Debian)
+standardSystem suite arch motd =
+	standardSystemUnhardened suite arch motd
+		`before` Ssh.noPasswords
+
+standardSystemUnhardened :: DebianSuite -> Architecture -> Motd -> Property (HasInfo + Debian)
+standardSystemUnhardened suite arch motd = propertyList "standard system" $ props
+	& osDebian suite arch
+	& Hostname.sane
+	& Hostname.searchDomain
+	& Locale.available "en_US.UTF-8"
+	& File.hasContent "/etc/motd" ("":motd++[""])
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.cacheCleaned
+	& Apt.installed ["etckeeper"]
+	& Apt.installed ["ssh", "mosh"]
+	& GitHome.installedFor (User "root")
+	& User.hasSomePassword (User "root")
+	& User.accountFor (User "joey")
+	& User.hasSomePassword (User "joey")
+	& Sudo.enabledFor (User "joey")
+	& GitHome.installedFor (User "joey")
+	& Apt.installed ["vim", "screen", "less"]
 	& Cron.runPropellor (Cron.Times "30 * * * *")
+	-- I use postfix, or no MTA.
+	& Apt.removed ["exim4", "exim4-daemon-light", "exim4-config", "exim4-base"]
+		`onChange` Apt.autoRemove
+	-- At least until system integration catches up, revert
+	-- systemd 230's behavior of enabling this property by default.
+	! Systemd.killUserProcesses
+
+-- This is my standard container setup, Featuring automatic upgrades.
+standardContainer :: DebianSuite -> Property (HasInfo + Debian)
+standardContainer suite = propertyList "standard container" $ props
+	& osDebian suite X86_64
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& Apt.cacheCleaned
+
+myDnsSecondary :: Property (HasInfo + DebianLike)
+myDnsSecondary = propertyList "dns secondary for all my domains" $ props
+	& Dns.secondary hosts "kitenet.net"
+	& Dns.secondary hosts "joeyh.name"
+	& Dns.secondary hosts "ikiwiki.info"
+	& Dns.secondary hosts "olduse.net"
+
+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.
+-- kite handles all mail.
+myDnsPrimary :: Bool -> Domain -> [(BindDomain, Record)] -> RevertableProperty (HasInfo + DebianLike) DebianLike
+myDnsPrimary dnssec domain extras = (if dnssec then Dns.signedPrimary (Weekly Nothing) else Dns.primary) 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.
+	[ host "usw-s002.rsync.net" $ props
+		& Ssh.hostPubKey SshEd25519 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB7yTEBGfQYdwG/oeL+U9XPMIh/dW7XNs9T+M79YIOrd"
+	, host "github.com" $ props
+		& Ssh.hostPubKey SshRsa "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
+	, host "gitlab.com" $ props
+		& Ssh.hostPubKey SshEcdsa "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY="
+	, host "ns6.gandi.net" $ props
+		& ipv4 "217.70.177.40"
+	, host "animx" $ props
+		& ipv4 "76.7.162.186"
+		& ipv4 "76.7.162.187"
+	]
+
+
+
+                          --                                o
+                          --             ___                 o              o
+                       {-----\          / o \              ___o            o
+                       {      \    __   \   /   _        (X___>--         __o
+  _____________________{ ______\___  \__/ | \__/ \____                  |X__>
+ <                                  \___//|\\___/\     \____________   _
+  \                                  ___/ | \___    # #             \ (-)
+   \    O      O      O             #     |     \ #                  >=)
+    \______________________________# #   /       #__________________/ (-}
+
+
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,22 @@
+propellor (5.3.0) unstable; urgency=medium
+
+  * Avoid bogus warning about new upstream version when /usr/bin/propellor
+    is run on a Debian system, but ~/.propellor was not cloned from the
+    Debian git bundle.
+  * Parted: Allow partitions to have no filesystem, for eg, GPT BIOS boot
+    partitions. (API change)
+  * Added rawPartition to PartSpec, for specifying partitions with no
+    filesystem.
+  * Added BiosGrubFlag to PartFlag.
+  * Add HasCallStack constraint to pickOS and unsupportedOS, so the
+    call stack includes the caller.
+  * Run su with --login, to avoid inheriting some problematic environment
+    variables, such as TMP, from the caller.
+  * Grub: Added properties to configure /etc/default/grub.
+  * Laptop: New module, starting with powertopAutoTuneOnBoot.
+
+ -- Joey Hess <id@joeyh.name>  Thu, 01 Feb 2018 12:27:01 -0400
+
 propellor (5.2.0) unstable; urgency=medium
 
   [ Joey Hess ]
diff --git a/joeyconfig.hs b/joeyconfig.hs
--- a/joeyconfig.hs
+++ b/joeyconfig.hs
@@ -27,11 +27,13 @@
 import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
 import qualified Propellor.Property.Locale as Locale
 import qualified Propellor.Property.Grub as Grub
+import qualified Propellor.Property.FlashKernel as FlashKernel
 import qualified Propellor.Property.Borg as Borg
 import qualified Propellor.Property.Gpg as Gpg
 import qualified Propellor.Property.Systemd as Systemd
 import qualified Propellor.Property.Journald as Journald
 import qualified Propellor.Property.Fail2Ban as Fail2Ban
+import qualified Propellor.Property.Laptop as Laptop
 import qualified Propellor.Property.OS as OS
 import qualified Propellor.Property.HostingProvider.CloudAtCost as CloudAtCost
 import qualified Propellor.Property.HostingProvider.Linode as Linode
@@ -58,6 +60,7 @@
 	, elephant
 	, beaver
 	, mouse
+	, peregrine
 	, pell
 	, keysafe
 	] ++ monsters
@@ -87,12 +90,16 @@
 	& ipv6 "2001:4830:1600:187::2"
 	& Hostname.sane
 	& Apt.serviceInstalledRunning "swapspace"
+	& Laptop.powertopAutoTuneOnBoot
+	& Grub.cmdline_Linux_default "i915.enable_psr=1"
+	! Grub.cmdline_Linux_default "quiet"
 
 	& JoeySites.dkimMilter
 	& JoeySites.postfixSaslPasswordClient
 	-- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
 	--	"/usr/bin/timeout 45m /home/joey/bin/goodmorning"
 	& 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")
 		]
@@ -114,7 +121,7 @@
 clam = host "clam.kitenet.net" $ props
 	& standardSystem Unstable X86_64
 		["Unreliable server. Anything here may be lost at any time!" ]
-	& ipv4 "45.62.211.6"
+	& ipv4 "45.62.211.94"
 
 	& CloudAtCost.decruft
 	& Ssh.hostKeys hostContext
@@ -186,6 +193,9 @@
 			`mountedAt` "/"
 			`setSize` MegaBytes 8000
 		)
+	& File.hasPrivContentExposed "/etc/flash-kernel/dtbs/sun7i-a20-cubietruck.dtb"
+		(Context "cubietruck gpio")
+		`onChange` FlashKernel.flashKernel
 	
 	& Apt.installed ["firmware-brcm80211"]
 		-- Workaround for https://bugs.debian.org/844056
@@ -315,6 +325,7 @@
 	& myDnsPrimary True "kitenet.net"
 		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
 		, (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
+		, (RelDomain "peregrine-onion", CNAME $ AbsDomain "ahw47zqw6qszoufl.onion")
 		]
 	& myDnsPrimary True "joeyh.name" []
 	& myDnsPrimary True "ikiwiki.info" []
@@ -415,6 +426,12 @@
 mouse :: Host
 mouse = host "mouse.kitenet.net" $ props
 	& ipv4 "67.223.19.96"
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+peregrine :: Host
+peregrine = host "peregrine.kitenet.net" $ props
 	& Apt.installed ["ssh"]
 	& Tor.installed
 	& Tor.hiddenServiceAvailable "ssh" (Port 22)
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 5.2.0
+Version: 5.3.0
 Cabal-Version: >= 1.20
 License: BSD2
 Maintainer: Joey Hess <id@joeyh.name>
@@ -126,6 +126,7 @@
     Propellor.Property.Installer.Target
     Propellor.Property.Journald
     Propellor.Property.Kerberos
+    Propellor.Property.Laptop
     Propellor.Property.LetsEncrypt
     Propellor.Property.List
     Propellor.Property.LightDM
diff --git a/src/Propellor/DotDir.hs b/src/Propellor/DotDir.hs
--- a/src/Propellor/DotDir.hs
+++ b/src/Propellor/DotDir.hs
@@ -358,7 +358,7 @@
 		withQuietOutput createProcessSuccess $
 			proc "git" ["log", headrev]
 	if (headknown == Nothing)
-		then setupUpstreamMaster headrev
+		then updateUpstreamMaster headrev
 		else do
 			theirhead <- getCurrentGitSha1 =<< getCurrentBranchRef
 			when (theirhead /= headrev) $ do
@@ -372,26 +372,30 @@
 		d <- dotPropellor
 		doesFileExist (d </> "propellor.cabal")
 
--- Makes upstream/master in dotPropellor be a usefully mergeable branch.
+-- Updates upstream/master in dotPropellor so merging from it will update
+-- to the latest distrepo.
 --
--- We cannot just use origin/master, because in the case of a distrepo,
--- it only contains 1 commit. So, trying to merge with it will result
--- in lots of merge conflicts, since git cannot find a common parent
--- commit.
+-- We cannot just fetch the distrepo because the distrepo contains only 
+-- 1 commit. So, trying to merge with it will result in lots of merge
+-- conflicts, since git cannot find a common parent commit.
 --
--- Instead, the upstream/master branch is created by taking the
--- upstream/master branch (which must be an old version of propellor,
+-- Instead, the new upstream/master branch is updated by taking the
+-- current upstream/master branch (which must be an old version of propellor,
 -- as distributed), and diffing from it to the current origin/master,
 -- and committing the result. This is done in a temporary clone of the
 -- repository, giving it a new master branch. That new branch is fetched
 -- into the user's repository, as if fetching from a upstream remote,
 -- yielding a new upstream/master branch.
-setupUpstreamMaster :: String -> IO ()
-setupUpstreamMaster newref = do
+--
+-- If there's no upstream/master, the user is not using the distrepo,
+-- so do nothing. And, if there's a remote named "upstream", the user
+-- must have set that up is not using the distrepo, so do nothing.
+updateUpstreamMaster :: String -> IO ()
+updateUpstreamMaster newref = unlessM (hasRemote "upstream") $ do
 	changeWorkingDirectory =<< dotPropellor
 	go =<< catchMaybeIO getoldrev
   where
-	go Nothing = warnoutofdate False
+	go Nothing = return ()
 	go (Just oldref) = do
 		let tmprepo = ".git/propellordisttmp"
 		let cleantmprepo = void $ catchMaybeIO $ removeDirectoryRecursive tmprepo
@@ -427,7 +431,7 @@
 warnoutofdate :: Bool -> IO ()
 warnoutofdate havebranch = do
 	warningMessage ("** Your ~/.propellor/ is out of date..")
-	let also s = hPutStrLn stderr ("   " ++ s)
+	let also s = infoMessage ["   " ++ s]
 	also ("A newer upstream version is available in " ++ distrepo)
 	if havebranch
 		then also ("To merge it, run: git merge " ++ upstreambranch)
diff --git a/src/Propellor/Git.hs b/src/Propellor/Git.hs
--- a/src/Propellor/Git.hs
+++ b/src/Propellor/Git.hs
@@ -23,9 +23,12 @@
 	<$> readProcess "git" ["show-ref", "--hash", branchref]
 
 hasOrigin :: IO Bool
-hasOrigin = catchDefaultIO False $ do
+hasOrigin = hasRemote "origin"
+
+hasRemote :: String -> IO Bool
+hasRemote remotename = catchDefaultIO False $ do
 	rs <- lines <$> readProcess "git" ["remote"]
-	return $ "origin" `elem` rs
+	return $ remotename `elem` rs
 
 hasGitRepo :: IO Bool
 hasGitRepo = doesFileExist ".git/HEAD"
diff --git a/src/Propellor/Property.hs b/src/Propellor/Property.hs
--- a/src/Propellor/Property.hs
+++ b/src/Propellor/Property.hs
@@ -55,6 +55,7 @@
 import Data.List
 import Data.Hashable
 import Control.Applicative
+import GHC.Stack
 import Prelude
 
 import Propellor.Types
@@ -283,6 +284,7 @@
 -- fail that way.
 pickOS
 	::
+		HasCallStack =>
 		( SingKind ('KProxy :: KProxy ka)
 		, SingKind ('KProxy :: KProxy kb)
 		, DemoteRep ('KProxy :: KProxy ka) ~ [MetaType]
@@ -344,7 +346,7 @@
 
 -- | Throws an error, for use in `withOS` when a property is lacking
 -- support for an OS.
-unsupportedOS' :: Propellor Result
+unsupportedOS' :: HasCallStack => Propellor Result
 unsupportedOS' = go =<< getOS
 	  where
 		go Nothing = error "Unknown host OS is not supported by this property."
diff --git a/src/Propellor/Property/Cmd.hs b/src/Propellor/Property/Cmd.hs
--- a/src/Propellor/Property/Cmd.hs
+++ b/src/Propellor/Property/Cmd.hs
@@ -94,6 +94,7 @@
 -- | A property that can satisfied by running a script
 -- as user (cd'd to their home directory).
 userScriptProperty :: User -> Script -> UncheckedProperty UnixLike
-userScriptProperty (User user) script = cmdProperty "su" ["--shell", "/bin/sh", "-c", shellcmd, user]
+userScriptProperty (User user) script = cmdProperty "su"
+	["--login", "--shell", "/bin/sh", "-c", shellcmd, user]
   where
 	shellcmd = intercalate " ; " ("set -e" : "cd" : script)
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
@@ -420,7 +420,7 @@
 	orderedmntsdevs = sortBy (compare `on` fst) $ zip mnts (zip mntopts devs)
 
 	swaps = map (SwapPartition . partitionLoopDev . snd) $
-		filter ((== LinuxSwap) . partFs . fst) $
+		filter ((== Just LinuxSwap) . partFs . fst) $
 			zip parts devs
 
 	mountall top = forM_ orderedmntsdevs $ \(mp, (mopts, loopdev)) -> case mp of
diff --git a/src/Propellor/Property/DiskImage/PartSpec.hs b/src/Propellor/Property/DiskImage/PartSpec.hs
--- a/src/Propellor/Property/DiskImage/PartSpec.hs
+++ b/src/Propellor/Property/DiskImage/PartSpec.hs
@@ -9,6 +9,7 @@
 	partition,
 	-- * PartSpec combinators
 	swapPartition,
+	rawPartition,
 	mountedAt,
 	addFreeSpace,
 	setSize,
@@ -48,11 +49,15 @@
 -- The partition is not mounted anywhere by default; use the combinators
 -- below to configure it.
 partition :: Monoid t => Fs -> PartSpec t
-partition fs = (Nothing, mempty, mkPartition fs, mempty)
+partition fs = (Nothing, mempty, mkPartition (Just fs), mempty)
 
 -- | Specifies a swap partition of a given size.
 swapPartition :: Monoid t => PartSize -> PartSpec t
-swapPartition sz = (Nothing, mempty, const (mkPartition LinuxSwap sz), mempty)
+swapPartition sz = (Nothing, mempty, const (mkPartition (Just LinuxSwap) sz), mempty)
+
+-- | Specifies a partition without any filesystem, of a given size.
+rawPartition :: Monoid t => PartSize -> PartSpec t
+rawPartition sz = (Nothing, mempty, const (mkPartition Nothing sz), mempty)
 
 -- | Specifies where to mount a partition.
 mountedAt :: PartSpec t -> MountPoint -> PartSpec t
diff --git a/src/Propellor/Property/FlashKernel.hs b/src/Propellor/Property/FlashKernel.hs
--- a/src/Propellor/Property/FlashKernel.hs
+++ b/src/Propellor/Property/FlashKernel.hs
@@ -23,9 +23,14 @@
 installed machine = setInfoProperty go (toInfo [FlashKernelInstalled])
   where
 	go = "/etc/flash-kernel/machine" `File.hasContent` [machine]
-		`onChange` (cmdProperty "flash-kernel" [] `assume` MadeChange)
+		`onChange` flashKernel
 		`requires` File.dirExists "/etc/flash-kernel"
 		`requires` Apt.installed ["flash-kernel"]
+
+-- | Runs flash-kernel with whatever machine `installed` configured.
+flashKernel :: Property DebianLike
+flashKernel = tightenTargets $
+	cmdProperty "flash-kernel" [] `assume` MadeChange
 
 -- | Runs flash-kernel in the system mounted at a particular directory.
 flashKernelMounted :: FilePath -> Property Linux
diff --git a/src/Propellor/Property/Gpg.hs b/src/Propellor/Property/Gpg.hs
--- a/src/Propellor/Property/Gpg.hs
+++ b/src/Propellor/Property/Gpg.hs
@@ -33,7 +33,7 @@
 			ifM (liftIO $ hasGpgKey (parse keylines))
 				( return NoChange
 				, makeChange $ withHandle StdinHandle createProcessSuccess
-					(proc "su" ["-c", "gpg --import", u]) $ \h -> do
+					(proc "su" ["--login", "-c", "gpg --import", u]) $ \h -> do
 						hPutStr h (unlines keylines)
 						hClose h
 				)
@@ -49,11 +49,11 @@
 
 hasPrivKey :: GpgKeyId -> User -> IO Bool
 hasPrivKey (GpgKeyId keyid) (User u) = catchBoolIO $
-	snd <$> processTranscript "su" ["-c", "gpg --list-secret-keys " ++ shellEscape keyid, u] Nothing
+	snd <$> processTranscript "su" ["--login", "-c", "gpg --list-secret-keys " ++ shellEscape keyid, u] Nothing
 
 hasPubKey :: GpgKeyId -> User -> IO Bool
 hasPubKey (GpgKeyId keyid) (User u) = catchBoolIO $
-	snd <$> processTranscript "su" ["-c", "gpg --list-public-keys " ++ shellEscape keyid, u] Nothing
+	snd <$> processTranscript "su" ["--login", "-c", "gpg --list-public-keys " ++ shellEscape keyid, u] Nothing
 
 dotDir :: User -> IO FilePath
 dotDir (User u) = do
diff --git a/src/Propellor/Property/Grub.hs b/src/Propellor/Property/Grub.hs
--- a/src/Propellor/Property/Grub.hs
+++ b/src/Propellor/Property/Grub.hs
@@ -5,6 +5,8 @@
 	installed,
 	mkConfig,
 	installed',
+	configured,
+	cmdline_Linux_default,
 	boots,
 	bootsMounted,
 	TimeoutSecs,
@@ -13,12 +15,16 @@
 
 import Propellor.Base
 import qualified Propellor.Property.File as File
+import qualified Propellor.Property.ConfFile as ConfFile
 import qualified Propellor.Property.Apt as Apt
 import Propellor.Property.Mount
 import Propellor.Property.Chroot (inChroot)
 import Propellor.Types.Info
 import Propellor.Types.Bootloader
+import Utility.SafeCommand
 
+import Data.List
+
 -- | Eg, \"hd0,0\" or \"xen/xvda1\"
 type GrubDevice = String
 
@@ -52,6 +58,65 @@
 		EFI32 -> "grub-efi-ia32"
 		Coreboot -> "grub-coreboot"
 		Xen -> "grub-xen"
+
+-- | Sets a simple confguration value, using grub-mkconfig to update
+-- the grub boot menu accordingly. On Debian, these are written to
+-- </etc/default/grub>
+--
+-- Example:
+--
+-- >	& Grub.configured "GRUB_TIMEOUT" "10"
+-- >	& Grub.configured "GRUB_TERMINAL_INPUT" "console serial"
+configured :: String -> String -> Property DebianLike
+configured k v = ConfFile.adjustSection 
+	("grub configured with " ++ k ++ "=" ++ v)
+	isline
+	(not . isline)
+	(const [l])
+	(const [l])
+	simpleConfigFile
+	`onChange` mkConfig
+  where
+	isline s = (k ++ "=") `isPrefixOf` s
+	l = k ++ "=" ++ shellEscape v
+
+simpleConfigFile :: FilePath
+simpleConfigFile = "/etc/default/grub"
+
+-- | Adds a word to the default linux command line.
+-- Any other words in the command line will be left unchanged.
+--
+-- Example:
+--
+-- > 	& Grub.cmdline_Linux_default "i915.enable_psr=1"
+-- > 	! Grub.cmdline_Linux_default "quiet"
+cmdline_Linux_default :: String -> RevertableProperty DebianLike DebianLike
+cmdline_Linux_default w = setup <!> undo
+  where
+	setup = ConfFile.adjustSection
+		("linux command line includes " ++ w)
+		isline
+		(not . isline)
+		(map (mkline . addw . getws))
+		(++ [mkline [w]])
+		simpleConfigFile
+		`onChange` mkConfig
+	undo = ConfFile.adjustSection
+		("linux command line does not include " ++ w)
+		isline
+		(not . isline)
+		(map (mkline . rmw . getws))
+		(++ [mkline [""]])
+		simpleConfigFile
+		`onChange` mkConfig
+	k = "GRUB_CMDLINE_LINUX_DEFAULT"
+	isline s = (k ++ "=") `isPrefixOf` s
+	mkline ws = k ++ "=" ++ shellEscape (unwords ws)
+	getws = concatMap words . shellUnEscape . drop 1 . dropWhile (/= '=')
+	addw ws
+		| w `elem` ws = ws
+		| otherwise = ws ++ [w]
+	rmw = filter (/= w)
 
 -- | Installs grub onto a device's boot loader, 
 -- so the system can boot from that device.
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
@@ -68,15 +68,19 @@
 -- see <https://git.joeyh.name/index.cgi/secret-project.git/>
 
 module Propellor.Property.Installer.Target (
+	-- * Main interface
 	TargetPartTable(..),
 	targetInstalled,
-	mountTarget,
 	fstabLists,
+	-- * Additional properties
+	mountTarget,
 	targetBootable,
 	partitionTargetDisk,
+	-- * Utility functions
 	targetDir,
 	probeDisk,
 	findDiskDevices,
+	-- * Installation progress tracking
 	TargetFilled,
 	TargetFilledHandle,
 	prepTargetFilled,
@@ -110,6 +114,7 @@
 import Data.Ratio
 import System.Process (readProcess)
 
+-- | Partition table for the target disk.
 data TargetPartTable = TargetPartTable TableType [PartSpec DiskPart]
 
 -- | Property that installs the target system to the TargetDiskDevice
@@ -179,6 +184,7 @@
 		umountaside = cmdProperty "umount" ["-l", "/mnt"]
 			`assume` MadeChange
 
+-- | Gets the target mounted.
 mountTarget
 	:: UserInput i
 	=> i
@@ -240,10 +246,10 @@
 	
 	partitions = map (\(mp, _, mkpart, _) -> (mp, mkpart mempty)) partspecs
 	mnts = mapMaybe fst $
-		filter (\(_, p) -> partFs p /= LinuxSwap) partitions
+		filter (\(_, p) -> partFs p /= Just LinuxSwap && partFs p /= Nothing) partitions
 	swaps targetdev = 
 		map (Fstab.SwapPartition . diskPartition targetdev . snd) $
-			filter (\((_, p), _) -> partFs p == LinuxSwap)
+			filter (\((_, p), _) -> partFs p == Just LinuxSwap)
 				(zip partitions partNums)
 
 -- | Make the target bootable using whatever bootloader is installed on it.
@@ -271,6 +277,7 @@
 				warningMessage $ "don't know how to enable bootloader(s) " ++ show l
 				return FailedChange
 
+-- | Partitions the target disk.
 partitionTargetDisk
 	:: UserInput i
 	=> i
@@ -424,10 +431,10 @@
 	parse _ = Nothing
 
 -- | How much of the target disks are used, compared with the size of the
--- installer's root device. Since the main part of an installation 
--- is rsyncing the latter to the former, this allows roughly estimating
--- the percent done while an install is running, and can be used in some
--- sort of progress display.
+-- installer's root device. Since the main part of an installation
+-- is `targetInstalled` rsyncing the latter to the former, this allows
+-- roughly estimating the percent done while an install is running,
+-- and can be used in some sort of progress display.
 data TargetFilled = TargetFilled (Ratio Integer)
 	deriving (Show, Eq)
 
@@ -437,6 +444,7 @@
 
 newtype TargetFilledHandle = TargetFilledHandle Integer
 
+-- | Prepare for getting `TargetFilled`.
 prepTargetFilled :: IO TargetFilledHandle
 prepTargetFilled = go =<< getMountSource "/"
   where
@@ -446,6 +454,8 @@
 		return (TargetFilledHandle sz)
 	go Nothing = return (TargetFilledHandle 0)
 
+-- | Get the current `TargetFilled` value. This is fast enough to be run
+-- multiple times per second without using much CPU.
 checkTargetFilled :: TargetFilledHandle -> IO TargetFilled
 checkTargetFilled (TargetFilledHandle installsz) = do
 	targetsz <- sum . map snd . filter (isTargetMountPoint . fst)
diff --git a/src/Propellor/Property/Laptop.hs b/src/Propellor/Property/Laptop.hs
new file mode 100644
--- /dev/null
+++ b/src/Propellor/Property/Laptop.hs
@@ -0,0 +1,28 @@
+module Propellor.Property.Laptop where
+
+import Propellor.Base
+import qualified Propellor.Property.File as File
+import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.Systemd as Systemd
+
+-- | Makes powertop auto-tune the system for optimal power consumption on
+-- boot.
+powertopAutoTuneOnBoot :: RevertableProperty DebianLike DebianLike
+powertopAutoTuneOnBoot = setup <!> undo
+	`describe` "powertop auto-tune on boot"
+  where
+	setup = Systemd.enabled "powertop"
+		`requires` Apt.installed ["powertop"]
+		`requires` File.hasContent servicefile
+			[ "[Unit]"
+			, "Description=Powertop tunings"
+			, "[Service]"
+			, "ExecStart=/usr/sbin/powertop --auto-tune"
+			, "RemainAfterExit=true"
+			, "[Install]"
+			, "WantedBy=multi-user.target"
+			]
+	undo = tightenTargets $ File.notPresent servicefile
+		`requires` check (doesFileExist servicefile)
+			(Systemd.disabled "powertop")
+	servicefile = "/etc/systemd/system/powertop.service"
diff --git a/src/Propellor/Property/Parted.hs b/src/Propellor/Property/Parted.hs
--- a/src/Propellor/Property/Parted.hs
+++ b/src/Propellor/Property/Parted.hs
@@ -62,8 +62,10 @@
   where
 	desc = disk ++ " partitioned"
 	formatl devs = combineProperties desc (toProps $ map format (zip parts devs))
-	format (p, dev) = Partition.formatted' (partMkFsOpts p)
-		Partition.YesReallyFormatPartition (partFs p) dev
+	format (p, dev) = case partFs p of
+		Just fs -> Partition.formatted' (partMkFsOpts p)
+			Partition.YesReallyFormatPartition fs dev
+		Nothing -> doNothing
 
 -- | Gets the total size of the disk specified by the partition table.
 partTableSize :: PartTable -> ByteSize
@@ -81,12 +83,12 @@
 		, pval f
 		, pval b
 		]
-	mkpart partnum startpos endpos p =
-		[ "mkpart"
-		, pval (partType p)
-		, pval (partFs p)
-		, partposexact startpos
-		, partposfuzzy endpos
+	mkpart partnum startpos endpos p = catMaybes
+		[ Just "mkpart"
+		, Just $ pval (partType p)
+		, fmap pval (partFs p)
+		, Just $ partposexact startpos
+		, Just $ partposfuzzy endpos
 		] ++ case partName p of
 			Just n -> ["name", show partnum, n]
 			Nothing -> []
diff --git a/src/Propellor/Property/Parted/Types.hs b/src/Propellor/Property/Parted/Types.hs
--- a/src/Propellor/Property/Parted/Types.hs
+++ b/src/Propellor/Property/Parted/Types.hs
@@ -31,7 +31,7 @@
 data Partition = Partition
 	{ partType :: PartType
 	, partSize :: PartSize
-	, partFs :: Partition.Fs
+	, partFs :: Maybe Partition.Fs
 	, partMkFsOpts :: Partition.MkfsOpts
 	, partFlags :: [(PartFlag, Bool)] -- ^ flags can be set or unset (parted may set some flags by default)
 	, partName :: Maybe String -- ^ optional name for partition (only works for GPT, PC98, MAC)
@@ -39,7 +39,7 @@
 	deriving (Show)
 
 -- | Makes a Partition with defaults for non-important values.
-mkPartition :: Partition.Fs -> PartSize -> Partition
+mkPartition :: Maybe Partition.Fs -> PartSize -> Partition
 mkPartition fs sz = Partition
 	{ partType = Primary
 	, partSize = sz
@@ -105,7 +105,7 @@
 fromAlignment (Alignment n) = n
 
 -- | Flags that can be set on a partition.
-data PartFlag = BootFlag | RootFlag | SwapFlag | HiddenFlag | RaidFlag | LvmFlag | LbaFlag | LegacyBootFlag | IrstFlag | EspFlag | PaloFlag
+data PartFlag = BootFlag | RootFlag | SwapFlag | HiddenFlag | RaidFlag | LvmFlag | LbaFlag | LegacyBootFlag | IrstFlag | EspFlag | PaloFlag | BiosGrubFlag
 	deriving (Show)
 
 instance PartedVal PartFlag where
@@ -120,6 +120,7 @@
 	pval IrstFlag = "irst"
 	pval EspFlag = "esp"
 	pval PaloFlag = "palo"
+	pval BiosGrubFlag = "bios_grub"
 
 instance PartedVal Bool where
 	pval True = "on"
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
@@ -948,10 +948,9 @@
 		, "[Install]"
 		, "WantedBy=multi-user.target"
 		]
-	-- Only upload when eth0 is up; eg the satellite internet is up.
 	-- Any changes to the rsync command will need my .authorized_keys
 	-- rsync server command to be updated too.
-	rsynccommand = "if ip route | grep '^default' | grep -q eth0; then rsync -e 'ssh -i" ++ sshkeyfile ++ "' -avz rrds/recent/ joey@kitenet.net:/srv/web/homepower.joeyh.name/rrds/recent/; fi"
+	rsynccommand = "rsync -e 'ssh -i" ++ sshkeyfile ++ "' -avz rrds/recent/ joey@kitenet.net:/srv/web/homepower.joeyh.name/rrds/recent/"
 
 -- My home router, running hostapd and dnsmasq for wlan0,
 -- with eth0 connected to a satellite modem, and a fallback ppp connection.
@@ -998,7 +997,7 @@
 	& Apt.installed ["ppp"]
 		`before` File.hasContent "/etc/ppp/peers/provider"
 			[ "user \"joeyh@arczip.com\""
-			, "connect \"/usr/sbin/chat -v -f /etc/chatscripts/pap -T 9734111\""
+			, "connect \"/usr/sbin/chat -v -f /etc/chatscripts/pap -T 3825441\""
 			, "/dev/ttyACM0"
 			, "115200"
 			, "noipdefault"
@@ -1037,23 +1036,25 @@
 
 laptopSoftware :: Property DebianLike
 laptopSoftware = Apt.installed
-	[ "procmeter3", "xfce4", "procmeter3", "unclutter"
+	[ "intel-microcode"
+	, "procmeter3", "xfce4", "procmeter3", "unclutter"
 	, "mplayer", "fbreader", "firefox", "chromium"
 	, "libdatetime-event-sunrise-perl", "libtime-duration-perl"
-	, "iftop", "network-manager", "gtk-redshift", "powertop"
+	, "network-manager", "gtk-redshift", "powertop"
 	, "gimp", "gthumb", "inkscape", "sozi", "xzgv", "hugin"
 	, "mpc", "mpd", "ncmpc", "sonata", "mpdtoys"
-	, "bsdgames", "nethack"
+	, "bsdgames", "nethack-console"
 	, "xmonad", "libghc-xmonad-dev", "libghc-xmonad-contrib-dev"
 	, "ttf-bitstream-vera"
 	, "mairix", "offlineimap", "mutt"
-	, "nmap"
+	, "nmap", "whois", "wireshark", "tcpdump", "iftop"
 	, "udevil", "pmount"
 	, "arbtt", "hledger", "bc"
 	, "apache2", "ikiwiki", "libhighlight-perl"
 	, "pal"
 	, "yeahconsole", "xkbset", "xinput"
-	, "assword", "pumpa", "vorbis-tools"
+	, "assword", "pumpa"
+	, "vorbis-tools", "audacity"
 	, "xul-ext-ublock-origin", "xul-ext-pdf.js", "xul-ext-status4evar"
 	, "vim-syntastic", "vim-fugitive"
 	, "adb", "gthumb"
@@ -1076,5 +1077,4 @@
 	, "hothasktags", "hdevtools", "hlint"
 	, "gdb", "dpkg-repack", "lintian"
 	, "pristine-tar", "github-backup"
-	, "kvm"
 	]
diff --git a/src/propellor-config.hs b/src/propellor-config.hs
--- a/src/propellor-config.hs
+++ b/src/propellor-config.hs
@@ -1,29 +1,658 @@
--- This is the main configuration file for Propellor, and is used to build
--- the propellor program.
+-- This is the live config file used by propellor's author.
+-- https://propellor.branchable.com/
+module Main where
 
 import Propellor
+import Propellor.Property.Scheduled
+import Propellor.Property.DiskImage
+import Propellor.Property.Chroot
+import Propellor.Property.Machine
+import Propellor.Property.Bootstrap
 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
 import qualified Propellor.Property.User as User
+import qualified Propellor.Property.Hostname as Hostname
+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
+import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
+import qualified Propellor.Property.Locale as Locale
+import qualified Propellor.Property.Grub as Grub
+import qualified Propellor.Property.FlashKernel as FlashKernel
+import qualified Propellor.Property.Borg as Borg
+import qualified Propellor.Property.Gpg as Gpg
+import qualified Propellor.Property.Systemd as Systemd
+import qualified Propellor.Property.Journald as Journald
+import qualified Propellor.Property.Fail2Ban as Fail2Ban
+import qualified Propellor.Property.Laptop as Laptop
+import qualified Propellor.Property.OS as OS
+import qualified Propellor.Property.HostingProvider.CloudAtCost as CloudAtCost
+import qualified Propellor.Property.HostingProvider.Linode as Linode
+import qualified Propellor.Property.HostingProvider.DigitalOcean as DigitalOcean
+import qualified Propellor.Property.SiteSpecific.GitHome as GitHome
+import qualified Propellor.Property.SiteSpecific.GitAnnexBuilder as GitAnnexBuilder
+import qualified Propellor.Property.SiteSpecific.Branchable as Branchable
+import qualified Propellor.Property.SiteSpecific.JoeySites as JoeySites
 
-main :: IO ()
-main = defaultMain hosts
+main :: IO ()           --     _         ______`|                       ,-.__
+main = defaultMain hosts --  /   \___-=O`/|O`/__|                      (____.'
+  {- Propellor            -- \          / | /    )          _.-"-._
+     Deployed -}          --  `/-==__ _/__|/__=-|          (       \_
+hosts :: [Host]          --   *             \ | |           '--------'
+hosts =                 --                  (o)  `
+	[ darkstar
+	, gnu
+	, dragon
+	, clam
+	, orca
+	, baleen
+	, honeybee
+	, kite
+	, elephant
+	, beaver
+	, mouse
+	, peregrine
+	, pell
+	, keysafe
+	] ++ monsters
 
--- The hosts propellor knows about.
-hosts :: [Host]
-hosts =
-	[ mybox
-	]
+testvm :: Host
+testvm = host "testvm.kitenet.net" $ props
+	& osDebian Unstable X86_64
+	& OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.net")
+	 	`onChange` postinstall
+	& Hostname.sane
+	& Hostname.searchDomain
+	& Apt.installed ["linux-image-amd64"]
+	& Apt.installed ["ssh"]
+	& User.hasPassword (User "root")
+  where
+	postinstall :: Property (HasInfo + DebianLike)
+	postinstall = propertyList "fixing up after clean install" $ props
+		& OS.preserveRootSshAuthorized
+		& OS.preserveResolvConf
+		& Apt.update
+		& Grub.boots "/dev/sda"
+			`requires` Grub.installed Grub.PC
 
--- An example host.
-mybox :: Host
-mybox = host "mybox.example.com" $ props
+darkstar :: Host
+darkstar = host "darkstar.kitenet.net" $ props
 	& osDebian Unstable X86_64
-	& Apt.stdSourcesList
+	& ipv6 "2001:4830:1600:187::2"
+	& Hostname.sane
+	& Apt.serviceInstalledRunning "swapspace"
+	& Laptop.powertopAutoTuneOnBoot
+	& Grub.cmdline_Linux_default "i915.enable_psr=1"
+	! Grub.cmdline_Linux_default "quiet"
+
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+	-- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
+	--	"/usr/bin/timeout 45m /home/joey/bin/goodmorning"
+	& 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")
+		]
+	-- & imageBuiltFor honeybee
+	--	(RawDiskImage "/srv/honeybee.img")
+	--	(Debootstrapped mempty)
+
+gnu :: Host
+gnu = host "gnu.kitenet.net" $ props
+	& Postfix.satellite
+
+dragon :: Host
+dragon = host "dragon.kitenet.net" $ props
+	& ipv6 "2001:4830:1600:187::2"
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+
+clam :: Host
+clam = host "clam.kitenet.net" $ props
+	& standardSystem Unstable X86_64
+		["Unreliable server. Anything here may be lost at any time!" ]
+	& ipv4 "45.62.211.94"
+
+	& CloudAtCost.decruft
+	& Ssh.hostKeys hostContext
+		[ (SshDsa, "ssh-dss AAAAB3NzaC1kc3MAAACBAI3WUq0RaigLlcUivgNG4sXpso2ORZkMvfqKz6zkc60L6dpxvWDNmZVEH8hEjxRSYG07NehcuOgQqeyFnS++xw1hdeGjf37JqCUH49i02lra3Zxv8oPpRxyeqe5MmuzUJhlWvBdlc3O/nqZ4bTUfnxMzSYWyy6++s/BpSHttZplNAAAAFQC1DE0vzgVeNAv9smHLObQWZFe2VQAAAIBECtpJry3GC8NVTFsTHDGWksluoFPIbKiZUFFztZGdM0AO2VwAbiJ6Au6M3VddGFANgTlni6d2/9yS919zO90TaFoIjywZeXhxE2CSuRfU7sx2hqDBk73jlycem/ER0sanFhzpHVpwmLfWneTXImWyq37vhAxatJANOtbj81vQ3AAAAIBV3lcyTT9xWg1Q4vERJbvyF8mCliwZmnIPa7ohveKkxlcgUk5d6dnaqFfjVaiXBPN3Qd08WXoQ/a9k3chBPT9nW2vWgzzM8l36j2MbHLmaxGwevAc9+vx4MXqvnGHzd2ex950mC33ct3j0fzMZlO6vqEsgD4CYmiASxhfefj+JCQ==")
+		, (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJybAjUPUWIhvVMmer8K5ZgdfI54DM6vc8Mzw+5KmVKL0TwkvzbR1HAB4heyMGtN1F8YzkWhsI3/Txh+MQUJ+i4u8SvSYc6D1q3j3ZyCi06wZ3DJS25tZrOM/thOOA1DFA4Hhb0uI/1Kg8PguNNNSMXn8F7q3F6cFQizYgszs6z6ktiST/BTC+IXWovhcnn2vQXXU8FTcTsqBFqA5dEjZbp1WDzqp3km84ZyXGmoVlpqzXeMvlkWTIshYiQjXIwPOkALzlGYjp1lw1OaxPVI1IGFcgCbIWQQWoCReb+genX2VaR+odAYXjaOdRx0lQj7UCPTBCpqMyzBMLtT5Yiaqh")
+		, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPhfvcOuw0Yt+MnsFc4TI2gWkKi62Eajxz+TgbHMO/uRTYF8c5V8fOI3o+J/3m5+lT0S5o8j8a7xIC3COvi+AVw=")
+		]
 	& Apt.unattendedUpgrades
-	& Apt.installed ["etckeeper"]
+	& Systemd.persistentJournal
+	& Journald.systemMaxUse "50MiB"
+	& Apt.serviceInstalledRunning "swapspace"
+
+	& Tor.isRelay
+	& Tor.named "kite1"
+	& Tor.bandwidthRate (Tor.PerMonth "400 GB")
+
+	& Systemd.nspawned oldusenetShellBox
+
+	& JoeySites.scrollBox
+	& alias "scroll.joeyh.name"
+	& alias "us.scroll.joeyh.name"
+
+baleen :: Host
+baleen = host "baleen.kitenet.net" $ props
+	& standardSystem Unstable X86_64 [ "New git-annex build box." ]
+
+	-- Not on public network; ssh access via bounce host.
+	& ipv4 "138.38.77.40"
+	
+	-- The root filesystem content may be lost if the VM is resized.
+	-- /dev/vdb contains persistent storage.
+	& Fstab.mounted "auto" "/dev/vdb" "/var/lib/container" mempty
+	
+	& Apt.unattendedUpgrades
+	& Postfix.satellite
+	& Apt.serviceInstalledRunning "ntp"
+	& Systemd.persistentJournal
+
+orca :: Host
+orca = host "orca.kitenet.net" $ props
+	& standardSystem Unstable X86_64 [ "Main git-annex build box." ]
+	& ipv4 "138.38.108.179"
+
+	& Apt.unattendedUpgrades
+	& Postfix.satellite
+	& Apt.serviceInstalledRunning "ntp"
+	& Systemd.persistentJournal
+
+	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
+		GitAnnexBuilder.standardAutoBuilder
+		Unstable X86_64 Nothing (Cron.Times "15 * * * *") "2h")
+	& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
+		GitAnnexBuilder.standardAutoBuilder
+		Unstable X86_32 Nothing (Cron.Times "30 * * * *") "2h")
+	& 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")
+
+honeybee :: Host
+honeybee = host "honeybee.kitenet.net" $ props
+	& standardSystem Testing ARMHF
+		[ "Home router and arm git-annex build box." ]
+	
+	& cubietech_Cubietruck
+	& hasPartition
+		( partition EXT4
+			`mountedAt` "/"
+			`setSize` MegaBytes 8000
+		)
+	& File.hasPrivContentExposed "/etc/flash-kernel/dtbs/sun7i-a20-cubietruck.dtb"
+		(Context "cubietruck gpio")
+		`onChange` FlashKernel.flashKernel
+	
+	& 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
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIS/hDYq1MAxfOBf49htym3BOYlx4Gk9SDpiHjv7u6IC")
+		]
+
+	& JoeySites.homePowerMonitor
+		(User "joey")
+		hosts
+		(Context "homepower.joeyh.name")
+		(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMAmVYddg/RgCbIj+cLcEiddeFXaYFnbEJ3uGj9G/EyV joey@honeybee")
+	& JoeySites.homeRouter
+	& Apt.installed ["mtr-tiny", "iftop", "screen"]
+	& Postfix.satellite
+
+	& check (not <$> inChroot) (setupRevertableProperty autobuilder)
+	-- In case compiler needs more than available ram
+	& Apt.serviceInstalledRunning "swapspace"
+  where
+	autobuilder = Systemd.nspawned $ GitAnnexBuilder.autoBuilderContainer
+		GitAnnexBuilder.armAutoBuilder
+		Unstable ARMEL Nothing (Cron.Times "15 10 * * *") "10h"
+
+-- This is not a complete description of kite, since it's a
+-- multiuser system with eg, user passwords that are not deployed
+-- with propellor.
+kite :: Host
+kite = host "kite.kitenet.net" $ props
+	& standardSystemUnhardened Testing X86_64 [ "Welcome to kite!" ]
+	& ipv4 "66.228.36.95"
+	& ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
+	& alias "kitenet.net"
+	& alias "wren.kitenet.net" -- temporary
+	& 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")
+		, (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA2QAJEuvbTmaN9ex9i9bjPhMGj+PHUYq2keIiaIImJ+8mo+yKSaGUxebG4tpuDPx6KZjdycyJt74IXfn1voGUrfzwaEY9NkqOP3v6OWTC3QeUGqDCeJ2ipslbEd9Ep9XBp+/ldDQm60D0XsIZdmDeN6MrHSbKF4fXv1bqpUoUILk=")
+		, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLF+dzqBJZix+CWUkAd3Bd3cofFCKwHMNRIfwx1G7dL4XFe6fMKxmrNetQcodo2edyufwoPmCPr3NmnwON9vyh0=")
+		, (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFZftKMnH/zH29BHMKbcBO4QsgTrstYFVhbrzrlRzBO3")
+		]
+
+	& Network.preserveStatic "eth0" `requires` Network.cleanInterfacesFile
+	& Apt.installed ["linux-image-amd64"]
+	& Linode.serialGrub
+	& Linode.mlocateEnabled
+	& Apt.unattendedUpgrades
+	& Systemd.installed
+	& Systemd.persistentJournal
+	& Journald.systemMaxUse "500MiB"
+	& Ssh.passwordAuthentication True
+	& Fail2Ban.installed -- since ssh password authentication is allowed
+	-- Allow ssh -R to forward ports via kite
+	& Ssh.setSshdConfig "GatewayPorts" "clientspecified"
+	& 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
+		[ "--exclude=/proc/*"
+		, "--exclude=/sys/*"
+		, "--exclude=/run/*"
+		, "--exclude=/tmp/*"
+		, "--exclude=/var/tmp/*"
+		, "--exclude=/var/cache/*"
+		, "--exclude=/home/joey/lib"
+		-- These directories are backed up and restored separately.
+		, "--exclude=/srv/git"
+		, "--exclude=/var/spool/oldusenet"
+		]
+		[ Borg.KeepDays 7
+		, Borg.KeepWeeks 4
+		, Borg.KeepMonths 6
+		]
+		`requires` Ssh.knownHost hosts "eubackup.kitenet.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")
+			]
+
+	& alias "smtp.kitenet.net"
+	& alias "imap.kitenet.net"
+	& alias "pop.kitenet.net"
+	& alias "mail.kitenet.net"
+	& JoeySites.kiteMailServer
+
+	& JoeySites.legacyWebSites
+	& File.ownerGroup "/srv/web" (User "joey") (Group "joey")
+	& Apt.installed ["analog"]
+
+	& alias "git.kitenet.net"
+	& alias "git.joeyh.name"
+	& JoeySites.gitServer hosts
+
+	& JoeySites.downloads hosts
+	& 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"
+		, "rss2email", "archivemail"
+		, "devscripts"
+		-- Some users have zsh as their login shell.
+		, "zsh"
+		]
+
+	& alias "nntp.olduse.net"
+	& JoeySites.oldUseNetServer hosts
+
+	& alias "ns4.kitenet.net"
+	& myDnsPrimary True "kitenet.net"
+		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
+		, (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
+		, (RelDomain "peregrine-onion", CNAME $ AbsDomain "ahw47zqw6qszoufl.onion")
+		]
+	& myDnsPrimary True "joeyh.name" []
+	& myDnsPrimary True "ikiwiki.info" []
+	& myDnsPrimary True "olduse.net"
+		[ (RelDomain "article", CNAME $ AbsDomain "virgil.koldfront.dk")
+		]
+	& alias "ns4.branchable.com"
+	& branchableSecondary
+	& Dns.secondaryFor ["animx"] hosts "animx.eu.org"
+	-- Use its own name server (amoung other things this avoids
+	-- spamassassin URIBL_BLOCKED.
+	& "/etc/resolv.conf" `File.hasContent`
+		[ "nameserver 127.0.0.1"
+		, "domain kitenet.net"
+		, "search kitenet.net"
+		]
+	& alias "debug-me.joeyh.name"
+	-- debug-me installed manually until package is available
+	& Systemd.enabled "debug-me"
+
+	-- testing
+	& 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/"
+
+mouse :: Host
+mouse = host "mouse.kitenet.net" $ props
+	& ipv4 "67.223.19.96"
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+peregrine :: Host
+peregrine = host "peregrine.kitenet.net" $ props
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+-- Branchable is not completely deployed with propellor yet.
+pell :: Host
+pell = host "pell.branchable.com" $ props
+	& alias "branchable.com"
+	& ipv4 "66.228.46.55"
+	& ipv6 "2600:3c03::f03c:91ff:fedf:c0e5"
+
+	-- All the websites I host at branchable that don't use
+	-- branchable.com dns.
+	& alias "olduse.net"
+	& alias "www.olduse.net"
+	& alias "www.kitenet.net"
+	& alias "joeyh.name"
+	& alias "www.joeyh.name"
+	& alias "campaign.joeyh.name"
+	& alias "ikiwiki.info"
+	& alias "www.ikiwiki.info"
+	& alias "git.ikiwiki.info"
+	& alias "l10n.ikiwiki.info"
+	& alias "dist-bugs.kitenet.net"
+	& alias "family.kitenet.net"
+
+	& osDebian (Stable "stretch") X86_64
+	& Apt.installed ["linux-image-686-pae"]
+	& Apt.unattendedUpgrades
+	& Branchable.server hosts
+	& Linode.serialGrub
+
+-- See https://joeyh.name/code/keysafe/servers/ for requirements.
+keysafe :: Host
+keysafe = host "keysafe.joeyh.name" $ props
+	& ipv4 "139.59.17.168"
+	& Hostname.sane
+	& osDebian (Stable "stretch") X86_64
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& DigitalOcean.distroKernel
+	-- This is a 500 mb VM, so need more ram to build propellor.
+	& Apt.serviceInstalledRunning "swapspace"
+	& Cron.runPropellor (Cron.Times "30 * * * *")
+	& Apt.installed ["etckeeper", "sudo"]
+	& Apt.removed ["nfs-common", "exim4", "exim4-base", "exim4-daemon-light", "rsyslog", "acpid", "rpcbind", "at"]
+
 	& User.hasSomePassword (User "root")
-	& File.dirExists "/var/www"
+	& User.accountFor (User "joey")
+	& User.hasSomePassword (User "joey")
+	& Sudo.enabledFor (User "joey")
+
+	& Ssh.installed
+	& Ssh.randomHostKeys
+	& User "root" `Ssh.authorizedKeysFrom` (User "joey", darkstar)
+	& User "joey" `Ssh.authorizedKeysFrom` (User "joey", darkstar)
+	& Ssh.noPasswords
+
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "keysafe" (Port 4242)
+		`requires` Tor.hiddenServiceData "keysafe" hostContext
+	& Tor.bandwidthRate (Tor.PerMonth "750 GB")
+
+	-- keysafe installed manually until package is available
+	& Systemd.enabled "keysafe"
+
+	& Gpg.keyImported (Gpg.GpgKeyId "CECE11AE") (User "root")
+	& Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
+	& Ssh.userKeys (User "root")
+		(Context "keysafe.joeyh.name")
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEx8bK9ZbXVEgEvxQeXLjnr9cGa/QvoB459aglP529My root@keysafe")
+		]
+	-- Note that this is not an incremental backup; it uploads the
+	-- whole content every time. So, only run weekly.
+	& Cron.niceJob "keysafe backup" Cron.Weekly (User "root") "/" backupcmd
+		`requires` Apt.installed ["rsync"]
+  where
+	datadir = "/var/lib/keysafe"
+	backupdir = "/var/backups/keysafe"
+	rsyncnetbackup = "2318@usw-s002.rsync.net:keysafe"
+	backupcmd = unwords
+		[ "keysafe --store-directory", datadir, "--backup-server", backupdir
+		, "&& rsync -a --delete --max-delete 3 ",  backupdir , rsyncnetbackup
+		]
+
+       --'                        __|II|      ,.
+     ----                      __|II|II|__   (  \_,/\
+--'-------'\o/-'-.-'-.-'-.- __|II|II|II|II|___/   __/ -'-.-'-.-'-.-'-.-'-.-'-
+-------------------------- |   [Containers]      / --------------------------
+-------------------------- :                    / ---------------------------
+--------------------------- \____, 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
+	& standardContainer (Stable "stretch")
+	& alias hn
+	& Git.cloned (User "root") "git://kitenet-net.branchable.com/" "/var/www/html"
+		(Just "remotes/origin/old-kitenet.net")
+	& Apache.installed
+	& Apache.listenPorts [p]
+	& Apache.virtualHost hn p "/var/www/html"
+	& Apache.siteDisabled "000-default"
+  where
+	p = Port 1994
+	hn = "ancient.kitenet.net"
+
+oldusenetShellBox :: Systemd.Container
+oldusenetShellBox = Systemd.debContainer "oldusenet-shellbox" $ props
+	& standardContainer (Stable "stretch")
+	& 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.
+standardSystem :: DebianSuite -> Architecture -> Motd -> Property (HasInfo + Debian)
+standardSystem suite arch motd =
+	standardSystemUnhardened suite arch motd
+		`before` Ssh.noPasswords
+
+standardSystemUnhardened :: DebianSuite -> Architecture -> Motd -> Property (HasInfo + Debian)
+standardSystemUnhardened suite arch motd = propertyList "standard system" $ props
+	& osDebian suite arch
+	& Hostname.sane
+	& Hostname.searchDomain
+	& Locale.available "en_US.UTF-8"
+	& File.hasContent "/etc/motd" ("":motd++[""])
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.cacheCleaned
+	& Apt.installed ["etckeeper"]
+	& Apt.installed ["ssh", "mosh"]
+	& GitHome.installedFor (User "root")
+	& User.hasSomePassword (User "root")
+	& User.accountFor (User "joey")
+	& User.hasSomePassword (User "joey")
+	& Sudo.enabledFor (User "joey")
+	& GitHome.installedFor (User "joey")
+	& Apt.installed ["vim", "screen", "less"]
 	& Cron.runPropellor (Cron.Times "30 * * * *")
+	-- I use postfix, or no MTA.
+	& Apt.removed ["exim4", "exim4-daemon-light", "exim4-config", "exim4-base"]
+		`onChange` Apt.autoRemove
+	-- At least until system integration catches up, revert
+	-- systemd 230's behavior of enabling this property by default.
+	! Systemd.killUserProcesses
+
+-- This is my standard container setup, Featuring automatic upgrades.
+standardContainer :: DebianSuite -> Property (HasInfo + Debian)
+standardContainer suite = propertyList "standard container" $ props
+	& osDebian suite X86_64
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& Apt.cacheCleaned
+
+myDnsSecondary :: Property (HasInfo + DebianLike)
+myDnsSecondary = propertyList "dns secondary for all my domains" $ props
+	& Dns.secondary hosts "kitenet.net"
+	& Dns.secondary hosts "joeyh.name"
+	& Dns.secondary hosts "ikiwiki.info"
+	& Dns.secondary hosts "olduse.net"
+
+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.
+-- kite handles all mail.
+myDnsPrimary :: Bool -> Domain -> [(BindDomain, Record)] -> RevertableProperty (HasInfo + DebianLike) DebianLike
+myDnsPrimary dnssec domain extras = (if dnssec then Dns.signedPrimary (Weekly Nothing) else Dns.primary) 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.
+	[ host "usw-s002.rsync.net" $ props
+		& Ssh.hostPubKey SshEd25519 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB7yTEBGfQYdwG/oeL+U9XPMIh/dW7XNs9T+M79YIOrd"
+	, host "github.com" $ props
+		& Ssh.hostPubKey SshRsa "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
+	, host "gitlab.com" $ props
+		& Ssh.hostPubKey SshEcdsa "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY="
+	, host "ns6.gandi.net" $ props
+		& ipv4 "217.70.177.40"
+	, host "animx" $ props
+		& ipv4 "76.7.162.186"
+		& ipv4 "76.7.162.187"
+	]
+
+
+
+                          --                                o
+                          --             ___                 o              o
+                       {-----\          / o \              ___o            o
+                       {      \    __   \   /   _        (X___>--         __o
+  _____________________{ ______\___  \__/ | \__/ \____                  |X__>
+ <                                  \___//|\\___/\     \____________   _
+  \                                  ___/ | \___    # #             \ (-)
+   \    O      O      O             #     |     \ #                  >=)
+    \______________________________# #   /       #__________________/ (-}
+
+
