diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,18 @@
+propellor (5.13) unstable; urgency=medium
+
+  * Network: Added support for network interfaces with several address
+    stanzas, eg ipv4 and ipv6.
+    Thanks, Nicolas Schodet
+  * Sudo.enabledFor: Deal with new @includedir syntax in sudoers file.
+  * Apt.securityUpdates: Stop generating testing-security lines, as
+    testing-security is unused per debian documentation.
+  * Utility.HumanNumber: Fix rounding bug that could result in
+    sometimes quite wrong values, eg "1.1 GB" when the input value was a
+    few bytes less than 2 GB. Properties in Ccache and Journald that
+    used it to generate config files were affected by this bug.
+
+ -- Joey Hess <id@joeyh.name>  Fri, 30 Jul 2021 10:02:19 -0400
+
 propellor (5.12) unstable; urgency=medium
 
   * Worked around a situation where ghc uses insane amounts of memory
diff --git a/config.hs b/config.hs
--- a/config.hs
+++ b/config.hs
@@ -1,29 +1,471 @@
--- 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.Ssh as Ssh
 import qualified Propellor.Property.Cron as Cron
+import qualified Propellor.Property.Sudo as Sudo
 import qualified Propellor.Property.User as User
-
-main :: IO ()
-main = defaultMain hosts
+import qualified Propellor.Property.Hostname as Hostname
+import qualified Propellor.Property.Tor as Tor
+import qualified Propellor.Property.Dns as Dns
+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.Borg as Borg
+import qualified Propellor.Property.OpenId as OpenId
+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.LightDM as LightDM
+import qualified Propellor.Property.HostingProvider.Linode as Linode
+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
 
--- The hosts propellor knows about.
-hosts :: [Host]
-hosts =
-	[ mybox
-	]
+main :: IO ()           --     _         ______`|                       ,-.__
+main = defaultMain hosts --  /   \___-=O`/|O`/__|                      (____.'
+  {- Propellor            -- \          / | /    )          _.-"-._
+     Deployed -}          --  `/-==__ _/__|/__=-|          (       \_
+hosts :: [Host]          --   *             \ | |           '--------'
+hosts =                 --                  (o)  `
+	[ darkstar
+	, dragon
+	, oyster
+	, orca
+	, honeybee
+	, kite
+	, beaver
+	, sow
+	, mouse
+	, peregrine
+	, pell
+	] ++ monsters
 
--- 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
+	& Hostname.mailname
+	& Apt.serviceInstalledRunning "swapspace"
+	& Laptop.powertopAutoTuneOnBoot
+	& Laptop.trimSSD
+	& Grub.cmdline_Linux_default "i915.enable_psr=1"
+	! Grub.cmdline_Linux_default "quiet"
+	& User.hasGroup (User "joey") (Group "dialout")
+
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+	-- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
+	--	"/usr/bin/timeout 45m /home/joey/bin/goodmorning"
+	& JoeySites.laptopSoftware
+	& JoeySites.userDirHtml
+	& Ssh.userKeys (User "joey") hostContext
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfFntnesZcYz2B2T41ay45igfckXRSh5uVffkuCQkLv joey@darkstar")
+		]
+	& imageBuiltFor honeybee
+		(RawDiskImage "/srv/honeybee.img")
+		(Debootstrapped mempty)
+
+dragon :: Host
+dragon = host "dragon.kitenet.net" $ props
+	& ipv6 "2001:4830:1600:187::2"
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+
+oyster :: Host
+oyster = host "oyster.kitenet.net" $ props
+	& standardSystem (Stable "buster") X86_64
+		["Unreliable server. Anything here may be lost at any time!" ]
+	& ipv4 "45.138.157.89"
+
+	& User.hasPassword (User "root")
 	& Apt.unattendedUpgrades
-	& Apt.installed ["etckeeper"]
+
+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.autoBuilderContainer
+		GitAnnexBuilder.standardAutoBuilder
+		Testing ARM64 Nothing (Cron.Times "1 * * * *") "4h")
+
+honeybee :: Host
+honeybee = host "honeybee.kitenet.net" $ props
+	& standardSystem Testing ARMHF
+		[ "Home router and arm git-annex build box." ]
+	& Apt.removed ["rsyslog"]
+	
+	& cubietech_Cubietruck
+	& hasPartition
+		( partition EXT3
+			`mountedAt` "/"
+			`setSize` MegaBytes 16000
+		)
+	& JoeySites.cubieTruckOneWire
+	& Systemd.persistentJournal
+	& Apt.installed ["firmware-misc-nonfree"] -- wifi
+	& Apt.serviceInstalledRunning "ntp" -- no hardware clock
+	& bootstrappedFrom GitRepoOutsideChroot
+	& Ssh.hostKeys hostContext
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIS/hDYq1MAxfOBf49htym3BOYlx4Gk9SDpiHjv7u6IC")
+		]
+	& Ssh.userKeys (User "joey") hostContext
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAYgEgsDmN26goPBGPN0HIvtkZfxlc996nPfBPDWxGuh")
+		]
+
+	& JoeySites.house
+		(User "joey")
+		hosts
+		(Context "house.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 <$> hasContainerCapability Systemd.FilesystemContained) 
+		(setupRevertableProperty autobuilder)
+	-- In case compiler needs more than available ram
+	& Apt.serviceInstalledRunning "swapspace"
+  where
+	autobuilder = Systemd.nspawned $ GitAnnexBuilder.autoBuilderContainer
+		(GitAnnexBuilder.armAutoBuilder GitAnnexBuilder.standardAutoBuilder)
+		Testing ARMEL Nothing (Cron.Times "15 15 * * *") "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"
+	& 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"]
+	& Apt.serviceInstalledRunning "swapspace"
+	& 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 "/" (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"
+		]
+		[ Borg.KeepDays 7
+		, Borg.KeepWeeks 4
+		, Borg.KeepMonths 3
+		]
+		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
+		`requires` Ssh.userKeys (User "root")
+			(Context "kite.kitenet.net")
+			[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKyciu8E8qspcO8lnTSUBAxUdYfmk7FaFlvT5yxUaP+0 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
+	& JoeySites.gitAnnexDistributor
+	& JoeySites.tmp
+
+	& Apt.installed
+		[ "git-annex", "myrepos"
+		, "build-essential", "make"
+		, "rss2email", "archivemail"
+		, "devscripts"
+		-- Some users have zsh as their login shell.
+		, "zsh"
+		]
+
+	& alias "znc.kitenet.net"
+	& JoeySites.ircBouncer
+
+	& alias "kgb.kitenet.net"
+	& JoeySites.kgbServer
+	
+	& Systemd.nspawned ancientKitenet
+	! Systemd.nspawned openidProvider
+	
+	& alias "podcatcher.kitenet.net"
+	& JoeySites.podcatcher
+
+	& JoeySites.scrollBox
+	& alias "scroll.joeyh.name"
+	& alias "us.scroll.joeyh.name"
+
+	& alias "ns4.kitenet.net"
+	& myDnsPrimary "kitenet.net"
+		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
+		, (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
+		, (RelDomain "peregrine-onion", CNAME $ AbsDomain "rsdwvaabir6ty2kdzblq7wdda26ib4fuc6hzxzwum75jbn6thqbojvid.onion")
+		, (RelDomain "sow-onion", CNAME $ AbsDomain "urt4g2tq32qktgtp.onion")
+		]
+	& myDnsPrimary "joeyh.name" []
+	& myDnsPrimary "ikiwiki.info" []
+	& myDnsPrimary "olduse.net"
+		[ (RelDomain "article", CNAME $ AbsDomain "virgil.koldfront.dk")
+		]
+	! myDnsPrimary "quarantimer.app" []
+	& 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"
+	& Apt.installed ["debug-me", "debug-me-server"]
+	& Systemd.enabled "debug-me"
+
+	-- testing
+	& Apache.httpsVirtualHost "letsencrypt.joeyh.name" "/var/www/html"
+		(LetsEncrypt.AgreeTOS (Just "id@joeyh.name"))
+	& alias "letsencrypt.joeyh.name"
+
+beaver :: Host
+beaver = host "beaver.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)
+
+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"
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+peregrine :: Host
+peregrine = host "peregrine.kitenet.net" $ props
+	& Apt.installed ["ssh", "screen", "variety", "git-annex"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+	& LightDM.autoLogin (User "desktop")
+
+-- 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 "buster") X86_32
+	& Apt.installed ["linux-image-686-pae"]
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& Branchable.server hosts
+	& Linode.serialGrub
+
+
+
+       --'                        __|II|      ,.
+     ----                      __|II|II|__   (  \_,/\
+--'-------'\o/-'-.-'-.-'-.- __|II|II|II|II|___/   __/ -'-.-'-.-'-.-'-.-'-.-'-
+-------------------------- |   [Containers]      / --------------------------
+-------------------------- :                    / ---------------------------
+--------------------------- \____, o          ,' ----------------------------
+---------------------------- '--,___________,'  -----------------------------
+
+-- Exhibit: kite's 90's website on port 1994.
+ancientKitenet :: Systemd.Container
+ancientKitenet = Systemd.debContainer "ancient-kitenet" $ props
+	& standardContainer (Stable "buster")
+	& 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"
+
+-- My own openid provider. Uses php, so containerized for security
+-- and administrative sanity.
+openidProvider :: Systemd.Container
+openidProvider = Systemd.debContainer "openid-provider" $ props
+	-- simpleid is not in buster
+	& standardContainer (Stable "stretch")
+	& alias hn
+	& OpenId.providerFor [User "joey", User "liw"] hn (Just (Port 8086))
+  where
+	hn = "openid.kitenet.net"
+
+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.mailname
+	& 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")
-	& File.dirExists "/var/www"
+	& 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.
+	& JoeySites.noExim
+
+-- This is my standard container setup, Featuring automatic upgrades.
+standardContainer :: DebianSuite -> Property (HasInfo + Debian)
+standardContainer suite = propertyList "standard container" $ props
+	& osDebian suite X86_64
+	-- Do not want to run mail daemon inside a random container..
+	& JoeySites.noExim
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& Apt.cacheCleaned
+
+branchableSecondary :: RevertableProperty (HasInfo + DebianLike) DebianLike
+branchableSecondary = Dns.secondaryFor ["branchable.com"] hosts "branchable.com"
+
+-- 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 "ns6.gandi.net")
+	, (RootDomain, MX 0 $ AbsDomain "kitenet.net")
+	, (RootDomain, TXT "v=spf1 a a:kitenet.net ~all")
+	, JoeySites.domainKey
+	] ++ extras
+
+-- 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 "ns6.gandi.net" $ props
+		& ipv4 "217.70.177.40"
+	, host "animx" $ props
+		& ipv4 "76.7.174.49"
+	]
+
+
+
+                          --                                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,18 @@
+propellor (5.13) unstable; urgency=medium
+
+  * Network: Added support for network interfaces with several address
+    stanzas, eg ipv4 and ipv6.
+    Thanks, Nicolas Schodet
+  * Sudo.enabledFor: Deal with new @includedir syntax in sudoers file.
+  * Apt.securityUpdates: Stop generating testing-security lines, as
+    testing-security is unused per debian documentation.
+  * Utility.HumanNumber: Fix rounding bug that could result in
+    sometimes quite wrong values, eg "1.1 GB" when the input value was a
+    few bytes less than 2 GB. Properties in Ccache and Journald that
+    used it to generate config files were affected by this bug.
+
+ -- Joey Hess <id@joeyh.name>  Fri, 30 Jul 2021 10:02:19 -0400
+
 propellor (5.12) unstable; urgency=medium
 
   * Worked around a situation where ghc uses insane amounts of memory
diff --git a/executables/propellor-config.hs b/executables/propellor-config.hs
--- a/executables/propellor-config.hs
+++ b/executables/propellor-config.hs
@@ -1,29 +1,471 @@
--- 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.Ssh as Ssh
 import qualified Propellor.Property.Cron as Cron
+import qualified Propellor.Property.Sudo as Sudo
 import qualified Propellor.Property.User as User
-
-main :: IO ()
-main = defaultMain hosts
+import qualified Propellor.Property.Hostname as Hostname
+import qualified Propellor.Property.Tor as Tor
+import qualified Propellor.Property.Dns as Dns
+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.Borg as Borg
+import qualified Propellor.Property.OpenId as OpenId
+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.LightDM as LightDM
+import qualified Propellor.Property.HostingProvider.Linode as Linode
+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
 
--- The hosts propellor knows about.
-hosts :: [Host]
-hosts =
-	[ mybox
-	]
+main :: IO ()           --     _         ______`|                       ,-.__
+main = defaultMain hosts --  /   \___-=O`/|O`/__|                      (____.'
+  {- Propellor            -- \          / | /    )          _.-"-._
+     Deployed -}          --  `/-==__ _/__|/__=-|          (       \_
+hosts :: [Host]          --   *             \ | |           '--------'
+hosts =                 --                  (o)  `
+	[ darkstar
+	, dragon
+	, oyster
+	, orca
+	, honeybee
+	, kite
+	, beaver
+	, sow
+	, mouse
+	, peregrine
+	, pell
+	] ++ monsters
 
--- 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
+	& Hostname.mailname
+	& Apt.serviceInstalledRunning "swapspace"
+	& Laptop.powertopAutoTuneOnBoot
+	& Laptop.trimSSD
+	& Grub.cmdline_Linux_default "i915.enable_psr=1"
+	! Grub.cmdline_Linux_default "quiet"
+	& User.hasGroup (User "joey") (Group "dialout")
+
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+	-- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
+	--	"/usr/bin/timeout 45m /home/joey/bin/goodmorning"
+	& JoeySites.laptopSoftware
+	& JoeySites.userDirHtml
+	& Ssh.userKeys (User "joey") hostContext
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfFntnesZcYz2B2T41ay45igfckXRSh5uVffkuCQkLv joey@darkstar")
+		]
+	& imageBuiltFor honeybee
+		(RawDiskImage "/srv/honeybee.img")
+		(Debootstrapped mempty)
+
+dragon :: Host
+dragon = host "dragon.kitenet.net" $ props
+	& ipv6 "2001:4830:1600:187::2"
+	& JoeySites.dkimMilter
+	& JoeySites.postfixSaslPasswordClient
+
+oyster :: Host
+oyster = host "oyster.kitenet.net" $ props
+	& standardSystem (Stable "buster") X86_64
+		["Unreliable server. Anything here may be lost at any time!" ]
+	& ipv4 "45.138.157.89"
+
+	& User.hasPassword (User "root")
 	& Apt.unattendedUpgrades
-	& Apt.installed ["etckeeper"]
+
+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.autoBuilderContainer
+		GitAnnexBuilder.standardAutoBuilder
+		Testing ARM64 Nothing (Cron.Times "1 * * * *") "4h")
+
+honeybee :: Host
+honeybee = host "honeybee.kitenet.net" $ props
+	& standardSystem Testing ARMHF
+		[ "Home router and arm git-annex build box." ]
+	& Apt.removed ["rsyslog"]
+	
+	& cubietech_Cubietruck
+	& hasPartition
+		( partition EXT3
+			`mountedAt` "/"
+			`setSize` MegaBytes 16000
+		)
+	& JoeySites.cubieTruckOneWire
+	& Systemd.persistentJournal
+	& Apt.installed ["firmware-misc-nonfree"] -- wifi
+	& Apt.serviceInstalledRunning "ntp" -- no hardware clock
+	& bootstrappedFrom GitRepoOutsideChroot
+	& Ssh.hostKeys hostContext
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIS/hDYq1MAxfOBf49htym3BOYlx4Gk9SDpiHjv7u6IC")
+		]
+	& Ssh.userKeys (User "joey") hostContext
+		[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAYgEgsDmN26goPBGPN0HIvtkZfxlc996nPfBPDWxGuh")
+		]
+
+	& JoeySites.house
+		(User "joey")
+		hosts
+		(Context "house.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 <$> hasContainerCapability Systemd.FilesystemContained) 
+		(setupRevertableProperty autobuilder)
+	-- In case compiler needs more than available ram
+	& Apt.serviceInstalledRunning "swapspace"
+  where
+	autobuilder = Systemd.nspawned $ GitAnnexBuilder.autoBuilderContainer
+		(GitAnnexBuilder.armAutoBuilder GitAnnexBuilder.standardAutoBuilder)
+		Testing ARMEL Nothing (Cron.Times "15 15 * * *") "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"
+	& 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"]
+	& Apt.serviceInstalledRunning "swapspace"
+	& 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 "/" (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"
+		]
+		[ Borg.KeepDays 7
+		, Borg.KeepWeeks 4
+		, Borg.KeepMonths 3
+		]
+		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
+		`requires` Ssh.userKeys (User "root")
+			(Context "kite.kitenet.net")
+			[ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKyciu8E8qspcO8lnTSUBAxUdYfmk7FaFlvT5yxUaP+0 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
+	& JoeySites.gitAnnexDistributor
+	& JoeySites.tmp
+
+	& Apt.installed
+		[ "git-annex", "myrepos"
+		, "build-essential", "make"
+		, "rss2email", "archivemail"
+		, "devscripts"
+		-- Some users have zsh as their login shell.
+		, "zsh"
+		]
+
+	& alias "znc.kitenet.net"
+	& JoeySites.ircBouncer
+
+	& alias "kgb.kitenet.net"
+	& JoeySites.kgbServer
+	
+	& Systemd.nspawned ancientKitenet
+	! Systemd.nspawned openidProvider
+	
+	& alias "podcatcher.kitenet.net"
+	& JoeySites.podcatcher
+
+	& JoeySites.scrollBox
+	& alias "scroll.joeyh.name"
+	& alias "us.scroll.joeyh.name"
+
+	& alias "ns4.kitenet.net"
+	& myDnsPrimary "kitenet.net"
+		[ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
+		, (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
+		, (RelDomain "peregrine-onion", CNAME $ AbsDomain "rsdwvaabir6ty2kdzblq7wdda26ib4fuc6hzxzwum75jbn6thqbojvid.onion")
+		, (RelDomain "sow-onion", CNAME $ AbsDomain "urt4g2tq32qktgtp.onion")
+		]
+	& myDnsPrimary "joeyh.name" []
+	& myDnsPrimary "ikiwiki.info" []
+	& myDnsPrimary "olduse.net"
+		[ (RelDomain "article", CNAME $ AbsDomain "virgil.koldfront.dk")
+		]
+	! myDnsPrimary "quarantimer.app" []
+	& 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"
+	& Apt.installed ["debug-me", "debug-me-server"]
+	& Systemd.enabled "debug-me"
+
+	-- testing
+	& Apache.httpsVirtualHost "letsencrypt.joeyh.name" "/var/www/html"
+		(LetsEncrypt.AgreeTOS (Just "id@joeyh.name"))
+	& alias "letsencrypt.joeyh.name"
+
+beaver :: Host
+beaver = host "beaver.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)
+
+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"
+	& Apt.installed ["ssh"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+
+peregrine :: Host
+peregrine = host "peregrine.kitenet.net" $ props
+	& Apt.installed ["ssh", "screen", "variety", "git-annex"]
+	& Tor.installed
+	& Tor.hiddenServiceAvailable "ssh" (Port 22)
+	& LightDM.autoLogin (User "desktop")
+
+-- 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 "buster") X86_32
+	& Apt.installed ["linux-image-686-pae"]
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& Branchable.server hosts
+	& Linode.serialGrub
+
+
+
+       --'                        __|II|      ,.
+     ----                      __|II|II|__   (  \_,/\
+--'-------'\o/-'-.-'-.-'-.- __|II|II|II|II|___/   __/ -'-.-'-.-'-.-'-.-'-.-'-
+-------------------------- |   [Containers]      / --------------------------
+-------------------------- :                    / ---------------------------
+--------------------------- \____, o          ,' ----------------------------
+---------------------------- '--,___________,'  -----------------------------
+
+-- Exhibit: kite's 90's website on port 1994.
+ancientKitenet :: Systemd.Container
+ancientKitenet = Systemd.debContainer "ancient-kitenet" $ props
+	& standardContainer (Stable "buster")
+	& 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"
+
+-- My own openid provider. Uses php, so containerized for security
+-- and administrative sanity.
+openidProvider :: Systemd.Container
+openidProvider = Systemd.debContainer "openid-provider" $ props
+	-- simpleid is not in buster
+	& standardContainer (Stable "stretch")
+	& alias hn
+	& OpenId.providerFor [User "joey", User "liw"] hn (Just (Port 8086))
+  where
+	hn = "openid.kitenet.net"
+
+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.mailname
+	& 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")
-	& File.dirExists "/var/www"
+	& 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.
+	& JoeySites.noExim
+
+-- This is my standard container setup, Featuring automatic upgrades.
+standardContainer :: DebianSuite -> Property (HasInfo + Debian)
+standardContainer suite = propertyList "standard container" $ props
+	& osDebian suite X86_64
+	-- Do not want to run mail daemon inside a random container..
+	& JoeySites.noExim
+	& Apt.stdSourcesList `onChange` Apt.upgrade
+	& Apt.unattendedUpgrades
+	& Apt.cacheCleaned
+
+branchableSecondary :: RevertableProperty (HasInfo + DebianLike) DebianLike
+branchableSecondary = Dns.secondaryFor ["branchable.com"] hosts "branchable.com"
+
+-- 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 "ns6.gandi.net")
+	, (RootDomain, MX 0 $ AbsDomain "kitenet.net")
+	, (RootDomain, TXT "v=spf1 a a:kitenet.net ~all")
+	, JoeySites.domainKey
+	] ++ extras
+
+-- 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 "ns6.gandi.net" $ props
+		& ipv4 "217.70.177.40"
+	, host "animx" $ props
+		& ipv4 "76.7.174.49"
+	]
+
+
+
+                          --                                o
+                          --             ___                 o              o
+                       {-----\          / o \              ___o            o
+                       {      \    __   \   /   _        (X___>--         __o
+  _____________________{ ______\___  \__/ | \__/ \____                  |X__>
+ <                                  \___//|\\___/\     \____________   _
+  \                                  ___/ | \___    # #             \ (-)
+   \    O      O      O             #     |     \ #                  >=)
+    \______________________________# #   /       #__________________/ (-}
+
+
diff --git a/joeyconfig.hs b/joeyconfig.hs
--- a/joeyconfig.hs
+++ b/joeyconfig.hs
@@ -16,7 +16,6 @@
 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.Git as Git
@@ -26,7 +25,6 @@
 import qualified Propellor.Property.Locale as Locale
 import qualified Propellor.Property.Grub as Grub
 import qualified Propellor.Property.Borg as Borg
-import qualified Propellor.Property.Gpg as Gpg
 import qualified Propellor.Property.OpenId as OpenId
 import qualified Propellor.Property.Systemd as Systemd
 import qualified Propellor.Property.Journald as Journald
@@ -34,7 +32,6 @@
 import qualified Propellor.Property.Laptop as Laptop
 import qualified Propellor.Property.LightDM as LightDM
 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
@@ -48,9 +45,8 @@
 hosts =                 --                  (o)  `
 	[ darkstar
 	, dragon
-	, clam
+	, oyster
 	, orca
-	, baleen
 	, honeybee
 	, kite
 	, beaver
@@ -58,8 +54,6 @@
 	, mouse
 	, peregrine
 	, pell
-	, keysafe
-	, quarantimer
 	] ++ monsters
 
 darkstar :: Host
@@ -87,9 +81,6 @@
 	& imageBuiltFor honeybee
 		(RawDiskImage "/srv/honeybee.img")
 		(Debootstrapped mempty)
-	! imageBuiltFor banana
-		(RawDiskImage "/srv/banana.img")
-		(Debootstrapped mempty)
 
 dragon :: Host
 dragon = host "dragon.kitenet.net" $ props
@@ -97,46 +88,15 @@
 	& JoeySites.dkimMilter
 	& JoeySites.postfixSaslPasswordClient
 
-clam :: Host
-clam = host "clam.kitenet.net" $ props
+oyster :: Host
+oyster = host "oyster.kitenet.net" $ props
 	& standardSystem (Stable "buster") X86_64
 		["Unreliable server. Anything here may be lost at any time!" ]
-	& ipv4 "46.36.36.189"
+	& ipv4 "45.138.157.89"
 
 	& User.hasPassword (User "root")
-	& Ssh.hostKeys hostContext
-		[ (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPhfvcOuw0Yt+MnsFc4TI2gWkKi62Eajxz+TgbHMO/uRTYF8c5V8fOI3o+J/3m5+lT0S5o8j8a7xIC3COvi+AVw=")
-		]
 	& Apt.unattendedUpgrades
 
-	-- & Tor.isRelay
-	-- & Tor.named "kite1"
-	-- & Tor.bandwidthRate (Tor.PerMonth "400 GB")
-	
-	& "/etc/resolv.conf" `File.hasContent`
-		[ "nameserver 8.8.8.8"
-		, "nameserver 8.8.4.4"
-		, "nameserver 1.1.1.1"
-		, "domain kitenet.net"
-		, "search kitenet.net"
-		]
-
-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." ]
@@ -160,17 +120,6 @@
 		GitAnnexBuilder.standardAutoBuilder
 		Testing ARM64 Nothing (Cron.Times "1 * * * *") "4h")
 
-banana :: Host
-banana = host "banana.kitenet.net" $ props
-	& lemaker_Banana_Pi
-	& hasPartition
-		( partition EXT4
-			`mountedAt` "/"
-			`setSize` MegaBytes 950
-		)
-	& osDebian Testing ARMHF
-	& User.hasInsecurePassword (User "root") "root"
-
 honeybee :: Host
 honeybee = host "honeybee.kitenet.net" $ props
 	& standardSystem Testing ARMHF
@@ -259,7 +208,6 @@
 		, "--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
@@ -298,10 +246,6 @@
 		, "zsh"
 		]
 
-	& alias "nntp.olduse.net"
-	& JoeySites.oldUseNetServer hosts
-	& Systemd.nspawned oldusenetShellBox
-	
 	& alias "znc.kitenet.net"
 	& JoeySites.ircBouncer
 
@@ -330,7 +274,7 @@
 	& myDnsPrimary "olduse.net"
 		[ (RelDomain "article", CNAME $ AbsDomain "virgil.koldfront.dk")
 		]
-	& myDnsPrimary "quarantimer.app" []
+	! myDnsPrimary "quarantimer.app" []
 	& alias "ns4.branchable.com"
 	& branchableSecondary
 	& Dns.secondaryFor ["animx"] hosts "animx.eu.org"
@@ -343,7 +287,7 @@
 		]
 	
 	& alias "debug-me.joeyh.name"
-	& Apt.installed ["debug-me"]
+	& Apt.installed ["debug-me", "debug-me-server"]
 	& Systemd.enabled "debug-me"
 
 	-- testing
@@ -408,91 +352,8 @@
 	& 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
-	& Hostname.mailname
-	& osDebian (Stable "buster") 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"]
-	& JoeySites.noExim
-	& Apt.removed ["nfs-common", "rsyslog", "acpid", "rpcbind", "at"]
 
-	& User.hasSomePassword (User "root")
-	& 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
-		]
-
-quarantimer :: Host
-quarantimer = host "quarantimer.app" $ props
-	& ipv4 "45.33.73.207"
-	& Hostname.sane
-	& Hostname.mailname
-	& osDebian (Stable "buster") X86_64
-	& Apt.stdSourcesList `onChange` Apt.upgrade
-	& Apt.unattendedUpgrades
-	& Cron.runPropellor (Cron.Times "30 * * * *")
-	& Apt.installed ["etckeeper", "sudo"]
-	& JoeySites.noExim
-
-	& User.hasSomePassword (User "root")
-	& 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
-
-	& LetsEncrypt.letsEncrypt (LetsEncrypt.AgreeTOS (Just "id@joeyh.name"))
-		"quarantimer.app" "/home/joey/quarantimer/static"
-	& Apt.installed ["screen", "git", "ghc"]
-	& Apt.installed ["zlib1g-dev", "imagemagick"]
-	-- (Installing quarantimer not yet automated)
-
-
-
        --'                        __|II|      ,.
      ----                      __|II|II|__   (  \_,/\
 --'-------'\o/-'-.-'-.-'-.- __|II|II|II|II|___/   __/ -'-.-'-.-'-.-'-.-'-.-'-
@@ -515,12 +376,6 @@
   where
 	p = Port 1994
 	hn = "ancient.kitenet.net"
-
-oldusenetShellBox :: Systemd.Container
-oldusenetShellBox = Systemd.debContainer "oldusenet-shellbox" $ props
-	& standardContainer (Stable "buster")
-	& alias "shell.olduse.net"
-	& JoeySites.oldUseNetShellBox
 
 -- My own openid provider. Uses php, so containerized for security
 -- and administrative sanity.
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 5.12
+Version: 5.13
 Cabal-Version: 1.20
 License: BSD2
 Maintainer: Joey Hess <id@joeyh.name>
@@ -39,7 +39,7 @@
   Description: Build with type-errors library for better error messages and less memory use
 
 Library
-  Default-Language: Haskell98
+  Default-Language: Haskell2010
   GHC-Options: -Wall -fno-warn-tabs -O0
   Default-Extensions: TypeOperators
   Hs-Source-Dirs: src
@@ -239,7 +239,7 @@
     Paths_propellor
 
 Executable propellor-config
-  Default-Language: Haskell98
+  Default-Language: Haskell2010
   Hs-Source-Dirs: executables
   Main-Is: propellor-config.hs
   GHC-Options: -threaded -Wall -fno-warn-tabs
@@ -247,7 +247,7 @@
   Build-Depends: propellor, base
 
 Executable propellor
-  Default-Language: Haskell98
+  Default-Language: Haskell2010
   Hs-Source-Dirs: executables
   Main-Is: wrapper.hs
   GHC-Options: -threaded -Wall -fno-warn-tabs
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
@@ -107,16 +107,15 @@
 stdArchiveLines :: Propellor SourcesGenerator
 stdArchiveLines = return . binandsrc =<< getMirror
 
--- | Only available for Stable and Testing
+-- | Only available for Stable suites, not for Testing or Unstable.
 securityUpdates :: SourcesGenerator
 securityUpdates suite
-	| isStable suite || suite == Testing =
+	| isStable suite =
 		let l = "deb http://security.debian.org/debian-security " ++ securitysuite ++ " " ++ unwords stdSections
 		in [l, srcLine l]
 	| otherwise = []
   where
 	securitysuite
-		| suite == Testing = "testing-security"
 		| suite `elem` map Stable releasesusingoldname =
 			showSuite suite ++ "/updates"
 		| otherwise = showSuite suite ++ "-security"
diff --git a/src/Propellor/Property/Network.hs b/src/Propellor/Property/Network.hs
--- a/src/Propellor/Property/Network.hs
+++ b/src/Propellor/Property/Network.hs
@@ -10,6 +10,9 @@
 -- | Options to put in a stanza of an ifupdown interfaces file.
 type InterfaceOptions = [(String, String)]
 
+-- | Stanza of an ifupdown interfaces file, with header lines and options.
+type InterfaceStanza = ([String], InterfaceOptions)
+
 ifUp :: Interface -> Property DebianLike
 ifUp iface = tightenTargets $ cmdProperty "ifup" [iface]
 	`assume` MadeChange
@@ -52,22 +55,29 @@
 
 static' :: Interface -> IPAddr -> Maybe Gateway -> InterfaceOptions -> Property DebianLike
 static' iface addr gateway options =
-	interfaceFileContains (interfaceDFile iface) headerlines options'
+	static'' iface [(addr, gateway, options)]
+
+-- | Configures an interface with several stanzas (IPv4 and IPv6 for example).
+static'' :: Interface -> [(IPAddr, Maybe Gateway, InterfaceOptions)] -> Property DebianLike
+static'' iface confs =
+	interfaceFileContains' (interfaceDFile iface) stanzas
 	`describe` ("static IP address for " ++ iface)
 	`requires` interfacesDEnabled
   where
-	headerlines =
+	stanzas = map stanza confs
+	stanza (addr, gateway, options) = (headerlines addr, options' addr gateway options)
+	headerlines addr =
 		[ "auto " ++ iface
-		, "iface " ++ iface ++ " " ++ inet ++ " static"
+		, "iface " ++ iface ++ " " ++ (inet addr) ++ " static"
 		]
-	options' = catMaybes
+	options' addr gateway options = catMaybes
 		[ Just $ ("address", val addr)
 		, case gateway of
-			Just (Gateway gaddr) -> 
+			Just (Gateway gaddr) ->
 				Just ("gateway", val gaddr)
 			Nothing -> Nothing
 		] ++ options
-	inet = case addr of
+	inet addr = case addr of
 		IPv4 _ -> "inet"
 		IPv6 _ -> "inet6"
 
@@ -149,8 +159,13 @@
 		`describe` "interfaces.d directory enabled"
 
 interfaceFileContains :: FilePath -> [String] -> InterfaceOptions -> Property DebianLike
-interfaceFileContains f headerlines options = tightenTargets $ hasContent f $
-	warning : headerlines ++ map fmt options
+interfaceFileContains f headerlines options =
+	interfaceFileContains' f [(headerlines, options)]
+
+interfaceFileContains' :: FilePath -> [InterfaceStanza] -> Property DebianLike
+interfaceFileContains' f stanzas = tightenTargets $ hasContent f $
+	warning : concatMap stanza stanzas
   where
+	stanza (headerlines, options) = headerlines ++ map fmt options
 	fmt (k, v) = "\t" ++ k ++ " " ++ v
 	warning = "# Deployed by propellor, do not edit."
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
@@ -67,7 +67,6 @@
 	& Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
 
 	& adminuser "joey"
-	& adminuser "liw"
   where
 	adminuser u = propertyList ("admin user " ++ u) $ props
 		& User.accountFor (User u)
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
@@ -97,86 +97,6 @@
 	s = d </> "login.sh"
 	g = d </> "game.sh"
 
-oldUseNetServer :: [Host] -> Property (HasInfo + DebianLike)
-oldUseNetServer hosts = propertyList "olduse.net server" $ props
-	& Apt.installed ["leafnode"]
-	& oldUseNetInstalled "oldusenet-server"
-	& oldUseNetBackup
-	& spoolsymlink
-	& "/etc/news/leafnode/config" `File.hasContent`
-		[ "# olduse.net configuration (deployed by propellor)"
-		, "expire = 1000000" -- no expiry via texpire
-		, "server = " -- no upstream server
-		, "debugmode = 1"
-		, "allowSTRANGERS = 42" -- lets anyone connect
-		, "nopost = 1" -- no new posting (just gather them)
-		]
-	& "/etc/hosts.deny" `File.lacksLine` "leafnode: ALL"
-	& Apt.serviceInstalledRunning "openbsd-inetd"
-	& File.notPresent "/etc/cron.daily/leafnode"
-	& File.notPresent "/etc/cron.d/leafnode"
-	& Cron.niceJob "oldusenet-expire" (Cron.Times "11 1 * * *") (User "news") newsspool expirecommand
-	& Cron.niceJob "oldusenet-uucp" (Cron.Times "*/5 * * * *") (User "news") "/" uucpcommand
-	& Apache.siteEnabled "nntp.olduse.net" nntpcfg
-  where
-	newsspool = "/var/spool/news"
-	datadir = "/var/spool/oldusenet"
-	expirecommand = intercalate ";"
-		[ "find \\( -path ./out.going -or -path ./interesting.groups -or -path './*/.overview' \\) -prune -or -type f -ctime +60  -print | xargs --no-run-if-empty rm"
-		, "find -type d -empty | xargs --no-run-if-empty rmdir"
-		]
-	uucpcommand = "/usr/bin/uucp " ++ datadir
-	nntpcfg = apachecfg "nntp.olduse.net"
-		[ "  DocumentRoot " ++ datadir ++ "/"
-		, "  <Directory " ++ datadir ++ "/>"
-		, "    Options Indexes FollowSymlinks"
-		, "    AllowOverride None"
-		, Apache.allowAll
-		, "  </Directory>"
-		]
-
-	spoolsymlink :: Property UnixLike
-	spoolsymlink = check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool)
-		(property "olduse.net spool in place" $ makeChange $ do
-			removeDirectoryRecursive newsspool
-			createSymbolicLink (datadir </> "news") newsspool
-		)
-
-	oldUseNetBackup :: Property (HasInfo + DebianLike)
-	oldUseNetBackup = Borg.backup datadir borgrepo 
-		(Cron.Times "33 4 * * *")
-		[]
-		[Borg.KeepDays 30]
-		`requires` Ssh.userKeyAt (Just keyfile)
-			(User "root")
-			(Context "olduse.net")
-			(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtHhNuO/abNTrc5pyWkwXz/F8FAHw8VcET0Yh4mjgOk root@kite")
-		`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
-oldUseNetShellBox = propertyList "olduse.net shellbox" $ props
-	& oldUseNetInstalled "oldusenet"
-	& Service.running "shellinabox"
-
-oldUseNetInstalled :: Apt.Package -> Property DebianLike
-oldUseNetInstalled pkg = check (not <$> Apt.isInstalled pkg) $
-	propertyList ("olduse.net " ++ pkg) $ props
-		& Apt.installed (words "build-essential devscripts debhelper git libncursesw5-dev libpcre3-dev pkg-config bison libicu-dev libidn11-dev libcanlock2-dev libuu-dev ghc libghc-ifelse-dev libghc-hxt-dev libghc-utf8-string-dev libghc-missingh-dev libghc-sha-dev haskell-stack")
-			`describe` "olduse.net build deps"
-		& scriptProperty
-			[ "rm -rf /root/tmp/oldusenet" -- idenpotency
-			, "git clone git://olduse.net/ /root/tmp/oldusenet/source"
-			, "cd /root/tmp/oldusenet/source/"
-			, "HOME=/root dpkg-buildpackage -us -uc"
-			, "dpkg -i ../" ++ pkg ++ "_*.deb || true"
-			, "apt-get -fy install" -- dependencies
-			, "rm -rf /root/tmp/oldusenet"
-			]
-			`assume` MadeChange
-			`describe` "olduse.net built"
-
 kgbServer :: Property (HasInfo + DebianLike)
 kgbServer = propertyList desc $ props
 	& Apt.serviceInstalledRunning "kgb-bot"
@@ -328,7 +248,7 @@
 	-- git-annex distribution signing key
 	& Gpg.keyImported (Gpg.GpgKeyId "89C809CB") (User "joey")
 	-- used for building rpms
-	& Apt.installed ["rpm", "createrepo"]
+	& Apt.installed ["rpm", "createrepo-c"]
   where
 	endpoint d = combineProperties ("endpoint " ++ d) $ props
 		& File.dirExists d
@@ -439,7 +359,7 @@
 	& Apt.serviceInstalledRunning "clamav-freshclam"
 	-- Workaround https://bugs.debian.org/569150
 	& Cron.niceJob "amavis-expire" Cron.Daily (User "root") "/"
-		"find /var/lib/amavis/virusmails/ -type f -ctime +7 -delete"
+		"find /var/lib/amavis/virusmails/ -type f -ctime +2 -delete"
 
 	& dkimInstalled
 
@@ -600,7 +520,10 @@
 		`onChange` (imapalpinescript `File.mode`
 			combineModes (readModes ++ executeModes))
 		`describe` "imap script for pine"
-	& Apt.serviceInstalledRunning "mailman"
+	-- XXX temporarily disabled installing as it's not available in
+	-- debian unstable any longer. Need to upgrade to mailman3
+	-- at some point. (nontrivial)
+	-- & Apt.serviceInstalledRunning "mailman"
 	-- Override the default http url. (Only affects new lists.)
 	& "/etc/mailman/mm_cfg.py" `File.containsLine`
 		"DEFAULT_URL_PATTERN = 'https://%s/cgi-bin/mailman/'"
@@ -1145,7 +1068,7 @@
 laptopSoftware :: Property DebianLike
 laptopSoftware = Apt.installed
 	[ "intel-microcode", "acpi"
-	, "procmeter3", "xfce4", "procmeter3", "unclutter"
+	, "procmeter3", "xfce4", "procmeter3", "unclutter-xfixes"
 	, "mplayer", "fbreader", "firefox", "chromium"
 	, "libdatetime-event-sunrise-perl", "libtime-duration-perl"
 	, "network-manager", "network-manager-openvpn-gnome", "openvpn"
@@ -1167,6 +1090,7 @@
 	, "vorbis-tools", "audacity"
 	, "ekiga"
 	, "bluez-firmware", "blueman", "pulseaudio-module-bluetooth"
+	, "fwupd"
 	, "xul-ext-ublock-origin", "xul-ext-pdf.js", "xul-ext-status4evar"
 	, "vim-syntastic", "vim-fugitive"
 	, "adb", "gthumb"
@@ -1254,6 +1178,10 @@
 		(USBHubPort hubvendor 4)
 		(USBDriveId wd "25a3")
 		(Just "archive")
+	& autoMountDrivePort "archive-14"
+		(USBHubPort hubvendor 2)
+		(USBDriveId wd "25a3")
+		Nothing
 	& autoMountDrive "passport" Nothing
 	& Apt.installed ["git-annex", "borgbackup"]
   where
@@ -1280,7 +1208,7 @@
 autoMountDrivePort label hp drive malias = propertyList desc $ props
 	& File.hasContent ("/etc/systemd/system/" ++ hub)
 		[ "[Unit]"
-		, "Description=Startech usb hub port " ++ show (hubPort hp)
+		, "Description=Startech usb hub port " ++ show (hubPort hp) ++ " vendor " ++ driveVendorId drive ++ " driveid " ++ driveProductId drive
 		, "PartOf=" ++ mount
 		, "[Service]"
 		, "Type=oneshot"
@@ -1307,7 +1235,7 @@
 	devfile = "/dev/disk/by-label/" ++ label
 	mountpoint = "/media/joey/" ++ label
 	desc = "auto mount with hub port power control " ++ mountpoint
-	hub = "startech-hub-port-" ++ show (hubPort hp) ++ ".service"
+	hub = "startech-hub-port-" ++ show (hubPort hp) ++ "-vendor-" ++ driveVendorId drive ++ "-drivedid-" ++ driveProductId drive ++ ".service"
 	mount = svcbase ++ ".mount"
 	svcbase = Systemd.escapePath mountpoint
 	selecthubport = unwords
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
@@ -27,7 +27,7 @@
 	setup = property' desc $ \w -> do
 		locked <- liftIO $ isLockedPassword user
 		ensureProperty w $ combineProperties desc $ props
-			& containsLine sudoers "#includedir /etc/sudoers.d"
+			& includessudoersd
 			& fileProperty desc
 				(modify locked . filter (wanted locked))
 				dfile
@@ -43,8 +43,19 @@
 		desc = u ++ " is not sudoer"
 	
 	removeconflicting = fileProperty "remove conflicting" (filter notuserline)
+	
+	-- Not reverted because this line is included by default.
+	includessudoersd = fileProperty (sudoers ++ " includes " ++ sudoersd) addl sudoers
+	  where
+		addl content = content ++ 
+			if l `notElem` content && oldl `notElem` content
+				then [l]
+				else []
+		l = "@includedir /etc/sudoers.d"
+		oldl = "#includedir /etc/sudoers.d"
 
 	sudoers = "/etc/sudoers"
+	sudoersd = "/etc/sudoers.d"
 	dfile = "/etc/sudoers.d/000users"
 	sudobaseline = u ++ " ALL=(ALL:ALL)"
 	notuserline l = not (sudobaseline `isPrefixOf` l)
diff --git a/src/Utility/HumanNumber.hs b/src/Utility/HumanNumber.hs
--- a/src/Utility/HumanNumber.hs
+++ b/src/Utility/HumanNumber.hs
@@ -1,21 +1,25 @@
 {- numbers for humans
  -
- - Copyright 2012-2013 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2021 Joey Hess <id@joeyh.name>
  -
  - License: BSD-2-clause
  -}
 
-module Utility.HumanNumber where
+module Utility.HumanNumber (showImprecise) where
 
 {- Displays a fractional value as a string with a limited number
  - of decimal digits. -}
 showImprecise :: RealFrac a => Int -> a -> String
 showImprecise precision n
-	| precision == 0 || remainder == 0 = show (round n :: Integer)
-	| otherwise = show int ++ "." ++ striptrailing0s (pad0s $ show remainder)
+	| precision == 0 || remainder' == 0 = show (round n :: Integer)
+	| otherwise = show int' ++ "." ++ striptrailing0s (pad0s $ show remainder')
   where
 	int :: Integer
 	(int, frac) = properFraction n
 	remainder = round (frac * 10 ^ precision) :: Integer
+	(int', remainder')
+		-- carry the 1
+		| remainder == 10 ^ precision = (int + 1, 0)
+		| otherwise = (int, remainder)
 	pad0s s = replicate (precision - length s) '0' ++ s
 	striptrailing0s = reverse . dropWhile (== '0') . reverse
