diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,16 @@
+propellor (0.8.2) unstable; urgency=medium
+
+  * Fix bug in File.containsLines that caused lines that were already in the
+    file to sometimes be appended to the end.
+  * Hostname.sane also configures /etc/mailname.
+  * Fixed Postfix.satellite to really configure relayhost = smtp.domain.
+  * Avoid reconfiguring postfix unncessarily when it already has a relayhost.
+  * Deal with apache 2.4's change in the name of site-available config files.
+  * Hostname aliases can now be used in several places, including --spin
+    and Ssh.knownHost.
+
+ -- Joey Hess <joeyh@debian.org>  Mon, 04 Aug 2014 01:12:19 -0400
+
 propellor (0.8.1) unstable; urgency=medium
 
   * Run apt-get update in initial bootstrap.
diff --git a/config-joey.hs b/config-joey.hs
--- a/config-joey.hs
+++ b/config-joey.hs
@@ -8,6 +8,7 @@
 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.Gpg as Gpg
 import qualified Propellor.Property.Cron as Cron
 import qualified Propellor.Property.Sudo as Sudo
 import qualified Propellor.Property.User as User
@@ -22,6 +23,7 @@
 import qualified Propellor.Property.Postfix as Postfix
 import qualified Propellor.Property.Service as Service
 import qualified Propellor.Property.Grub as Grub
+import qualified Propellor.Property.Obnam as Obnam
 import qualified Propellor.Property.HostingProvider.DigitalOcean as DigitalOcean
 import qualified Propellor.Property.HostingProvider.CloudAtCost as CloudAtCost
 import qualified Propellor.Property.HostingProvider.Linode as Linode
@@ -41,7 +43,7 @@
 
 		& Apt.buildDep ["git-annex"] `period` Daily
 		& Docker.configured
-		& Docker.docked hosts "android-git-annex"
+		! Docker.docked hosts "android-git-annex"
 
 	, standardSystem "clam.kitenet.net" Unstable "amd64"
 		[ "Unreliable server. Anything here may be lost at any time!" ]
@@ -61,38 +63,70 @@
 		[ "Main git-annex build box." ]
 		& ipv4 "138.38.108.179"
 
-		& Hostname.sane
 		& Apt.unattendedUpgrades
 		& Postfix.satellite
 		& Docker.configured
 		& Docker.docked hosts "amd64-git-annex-builder"
 		& Docker.docked hosts "i386-git-annex-builder"
-		& Docker.docked hosts "armel-git-annex-builder-companion"
-		& Docker.docked hosts "armel-git-annex-builder"
 		& Docker.docked hosts "android-git-annex-builder"
+		-- not currently working
+		! Docker.docked hosts "armel-git-annex-builder-companion"
+		! Docker.docked hosts "armel-git-annex-builder"
 		& Docker.garbageCollected `period` Daily
 		& Apt.buildDep ["git-annex"] `period` Daily
 	
-  	, standardSystem "kite.kitenet.net" Unstable "amd64"
+	-- This is not a complete description of kite, since it's a
+	-- multiuser system with eg, user passwords that are not deployed
+	-- with propellor.
+  	, standardSystemUnhardened "kite.kitenet.net" Unstable "amd64"
 		[ "Welcome to the new kitenet.net server!"
 		, "This is still under construction and not yet live.."
 		]
 	  	& ipv4 "66.228.36.95"
 		& ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
+		-- & alias "kitenet.net" -- not yet live!
 
 		& Apt.installed ["linux-image-amd64"]
 		& Linode.chainPVGrub 5
-		& Hostname.sane
 		& Apt.unattendedUpgrades
 		& Apt.installed ["systemd"]
 		& Ssh.hostKeys (Context "kitenet.net")
-	
+		-- Since ssh password authentication is allowed:
+		& Apt.serviceInstalledRunning "fail2ban"
+		& Obnam.backup "/" "33 1 * * *"
+			[ "--repository=sftp://joey@eubackup.kitenet.net/~/lib/backup/kite.obnam"
+			, "--client-name=kitenet.net"
+			, "--encrypt-with="
+			, "--exclude=/var/cache"
+			, "--exclude=/var/tmp"
+			, "--exclude=/home/joey/lib"
+			, "--exclude=.*/tmp/"
+			, "--one-file-system"
+			] Obnam.OnlyClient
+			`requires` Gpg.keyImported "98147487" "root"
+			`requires` Ssh.keyImported SshRsa "root"
+				(Context "kite.kitenet.net")
+			`requires` Ssh.knownHost hosts "eubackup.kitenet.net" "root"
+
+		-- & alias "smtp.kitenet.net" -- not yet live!
+		-- & alias "imap.kitenet.net" -- not yet live!
+		-- & alias "mail.kitenet.net" -- not yet live!
+		& JoeySites.kiteMailServer
+
+		& JoeySites.legacyWebSites
+
+		& Apt.installed
+			["git-annex", "myrepos"
+			, "build-essential", "make"
+			-- Some users have zsh as their login shell.
+			, "zsh"
+			]
+
   	, standardSystem "diatom.kitenet.net" Stable "amd64"
 	  	[ "Important stuff that needs not too much memory or CPU." ]
 		& ipv4 "107.170.31.195"
 
 		& DigitalOcean.distroKernel
-		& Hostname.sane
 		& Ssh.hostKeys (Context "diatom.kitenet.net")
 		& Apt.unattendedUpgrades
 		& Apt.serviceInstalledRunning "ntp"
@@ -103,10 +137,7 @@
 		& Apt.serviceInstalledRunning "swapspace"
 	
 		& Apt.serviceInstalledRunning "apache2"
-		& File.hasPrivContent "/etc/ssl/certs/web.pem" (Context "kitenet.net")
-		& File.hasPrivContent "/etc/ssl/private/web.pem" (Context "kitenet.net")
-		& File.hasPrivContent "/etc/ssl/certs/startssl.pem" (Context "kitenet.net")
-		& Apache.modEnabled "ssl"
+		& JoeySites.kitenetHttps
 		& Apache.multiSSL
 		& File.ownerGroup "/srv/web" "joey" "joey"
 		& Apt.installed ["analog"]
@@ -116,14 +147,16 @@
 		& JoeySites.gitServer hosts
 	
 		& alias "downloads.kitenet.net"
-		& JoeySites.annexWebSite hosts "/srv/git/downloads.git"
+		& JoeySites.annexWebSite "/srv/git/downloads.git"
 			"downloads.kitenet.net"
 			"840760dc-08f0-11e2-8c61-576b7e66acfd"
-			[("turtle", "ssh://turtle.kitenet.net/~/lib/downloads/")]
+			[("usbackup", "ssh://usbackup.kitenet.net/~/lib/downloads/")]
+			`requires` Ssh.keyImported SshRsa "joey" (Context "downloads.kitenet.net")
+			`requires` Ssh.knownHost hosts "usbackup.kitenet.net" "joey"
 		& JoeySites.gitAnnexDistributor
 
 		& alias "tmp.kitenet.net"
-		& JoeySites.annexWebSite hosts "/srv/git/joey/tmp.git"
+		& JoeySites.annexWebSite "/srv/git/joey/tmp.git"
 			"tmp.kitenet.net"
 			"26fd6e38-1226-11e2-a75f-ff007033bdba"
 			[]
@@ -148,25 +181,28 @@
 
 	, let ctx = Context "elephant.kitenet.net"
 	  in standardSystem "elephant.kitenet.net" Unstable "amd64"
-	  	[ "Storage, big data, and backups, omnomnom!" ]
+	  	[ "Storage, big data, and backups, omnomnom!"
+		, "(Encrypt all data stored here.)"
+		]
 		& ipv4 "193.234.225.114"
 
 		& Grub.chainPVGrub "hd0,0" "xen/xvda1" 30
-		& Hostname.sane
 		& Postfix.satellite
 		& Apt.unattendedUpgrades
 		& Ssh.hostKeys ctx
+		& sshPubKey "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJkoPRhUGT8EId6m37uBdYEtq42VNwslKnc9mmO+89ody066q6seHKeFY6ImfwjcyIjM30RTzEwftuVNQnbEB0="
 		& Ssh.keyImported SshRsa "joey" ctx
 		& Apt.serviceInstalledRunning "swapspace"
 
 		& alias "eubackup.kitenet.net"
 		& Apt.installed ["obnam", "sshfs", "rsync"]
+		& JoeySites.obnamRepos ["wren", "pell", "kite"]
 		& JoeySites.githubBackup
-		& JoeySites.obnamRepos ["wren", "pell"]
-		& Ssh.knownHost hosts "usw-s002.rsync.net" "joey"
+		& JoeySites.rsyncNetBackup hosts
+		& JoeySites.backupsBackedupTo hosts "usbackup.kitenet.net" "lib/backup/eubackup"
 
 		& alias "podcatcher.kitenet.net"
-		& Apt.installed ["git-annex"]
+		& JoeySites.podcatcher
 		
 		& alias "znc.kitenet.net"
 		& JoeySites.ircBouncer
@@ -201,9 +237,9 @@
 			`onChange` Service.restarted "ssh"
 		
 		-- temp
-		& Docker.docked hosts "amd64-git-annex-builder"
-		& Docker.docked hosts "i386-git-annex-builder"
-		& Docker.docked hosts "android-git-annex-builder"
+		! Docker.docked hosts "amd64-git-annex-builder"
+		! Docker.docked hosts "i386-git-annex-builder"
+		! Docker.docked hosts "android-git-annex-builder"
 
 
 	    --'                        __|II|      ,.
@@ -261,8 +297,17 @@
 
 -- This is my standard system setup.
 standardSystem :: HostName -> DebianSuite -> Architecture -> Motd -> Host
-standardSystem hn suite arch motd = host hn
+standardSystem hn suite arch motd = standardSystemUnhardened hn suite arch motd
+	-- Harden the system, but only once root's authorized_keys
+	-- is safely in place.
+	& check (Ssh.hasAuthorizedKeys "root")
+		(Ssh.passwordAuthentication False)
+
+standardSystemUnhardened :: HostName -> DebianSuite -> Architecture -> Motd -> Host
+standardSystemUnhardened hn suite arch motd = host hn
 	& os (System (Debian suite) arch)
+	& Hostname.sane
+	& Hostname.searchDomain
 	& File.hasContent "/etc/motd" ("":motd++[""])
 	& Apt.stdSourcesList `onChange` Apt.upgrade
 	& Apt.cacheCleaned
@@ -270,10 +315,6 @@
 	& Apt.installed ["ssh"]
 	& GitHome.installedFor "root"
 	& User.hasSomePassword "root" (Context hn)
-	-- Harden the system, but only once root's authorized_keys
-	-- is safely in place.
-	& check (Ssh.hasAuthorizedKeys "root")
-		(Ssh.passwordAuthentication False)
 	& User.accountFor "joey"
 	& User.hasSomePassword "joey" (Context hn)
 	& Sudo.enabledFor "joey"
@@ -350,6 +391,7 @@
 		& ipv4 "67.223.19.96"
 		& ipv6 "2001:4978:f:2d9::2"
 		& alias "backup.kitenet.net"
+		& alias "usbackup.kitenet.net"
 		& sshPubKey "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAokMXQiX/NZjA1UbhMdgAscnS5dsmy+Q7bWrQ6tsTZ/o+6N/T5cbjoBHOdpypXJI3y/PiJTDJaQtXIhLa8gFg/EvxMnMz/KG9skADW1361JmfCc4BxicQIO2IOOe6eilPr+YsnOwiHwL0vpUnuty39cppuMWVD25GzxXlS6KQsLCvXLzxLLuNnGC43UAM0q4UwQxDtAZEK1dH2o3HMWhgMP2qEQupc24dbhpO3ecxh2C9678a3oGDuDuNf7mLp3s7ptj5qF3onitpJ82U5o7VajaHoygMaSRFeWxP2c13eM57j3bLdLwxVXFhePcKXARu1iuFTLS5uUf3hN6MkQcOGw=="
 	, host "wren.kitenet.net"
 		& ipv4 "80.68.85.49"
@@ -359,37 +401,46 @@
 		& alias "ftp.kitenet.net"
 		& alias "mail.kitenet.net"
 		& alias "smtp.kitenet.net"
-		& alias "sows-ear.kitenet.net"
-		& alias "www.sows-ear.kitenet.net"
-		& alias "wortroot.kitenet.net"
-		& alias "www.wortroot.kitenet.net"
-		& alias "joey.kitenet.net"
-		& alias "anna.kitenet.net"
 		& alias "bitlbee.kitenet.net"
 		{- Remaining services on kite:
+		 -
+		 - / = ready to go on kite.kitenet.net
 		 - 
 		 - mail
-		 -   postfix
-		 -   postgrey
+		 -   /postfix
+		 -   /postgrey
 		 -   mailman
-		 -   spamassassin
-		 -   sqwebmail
-		 -   courier
-		 -     imap
-		 -     tls
-		 - apache
-		 -   some static websites
-		 - bitlbee
-		 - prosody
-		 -   (used by daddy's git-annex)
-		 - named
-		 -   (branchable is still pushing to here
-		 -    (thinking it's ns2.branchable.com), but it's no
-		 -   longer a primary or secondary for anything)
+		 -   /spamassassin
+		 -   sqwebmail (cannot use this with dovecot, alternatives?)
+		 -   /imap server
+		 -   /pop server
+		 - /apache
+		 -   (need to re-rsync /srv/web to new kite.kitenet.net
+		 -   server before decommissioning)
+		 - bitlbee (EOL?)
+		 - prosody (EOL?)
 		 - ftpd (EOL)
 		 -
-		 - user shell stuff:
-		 -   pine, zsh, make, git-annex, myrepos, ...
+		 - Pre-transition:
+		 - - re-rsync /home (skip ~joey and .pine*)
+		 - 
+		 - Transition plan:
+		 - - on darkstar: offlineimap run & disable cron job
+		 -    & move offlineimap files to tmp
+		 - - take down wren pstfix, imap, pop servers
+		 - - log all users out of wren
+		 - - final /home rsync (skip ~joey and .pine*)
+		 - - rsync /var/mail
+		 - - rsync mailman and mailman list archives dirs
+		 - - switch kitenet.net dns and enable pop.kitenet.net etc aliass
+		 - - point wren.kitenet.net at kite.kitenet.net temporarily
+		 -   (make real-wren.kitenet.net alias)
+		 - - reconfigure errol's email client to use new server
+		 - - re-run offlinimap against new server
+		 - - test mail
+		 - - test virus filtering
+		 - - test http://kitenet.net/~kyle/ (user home dirs)
+		 - - migrate user cron jobs
 		 -}
 	, host "mouse.kitenet.net"
 		& ipv6 "2001:4830:1600:492::2"
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
+propellor (0.8.2) unstable; urgency=medium
+
+  * Fix bug in File.containsLines that caused lines that were already in the
+    file to sometimes be appended to the end.
+  * Hostname.sane also configures /etc/mailname.
+  * Fixed Postfix.satellite to really configure relayhost = smtp.domain.
+  * Avoid reconfiguring postfix unncessarily when it already has a relayhost.
+  * Deal with apache 2.4's change in the name of site-available config files.
+  * Hostname aliases can now be used in several places, including --spin
+    and Ssh.knownHost.
+
+ -- Joey Hess <joeyh@debian.org>  Mon, 04 Aug 2014 01:12:19 -0400
+
 propellor (0.8.1) unstable; urgency=medium
 
   * Run apt-get update in initial bootstrap.
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 0.8.1
+Version: 0.8.2
 Cabal-Version: >= 1.6
 License: BSD3
 Maintainer: Joey Hess <joey@kitenet.net>
diff --git a/src/Propellor/Info.hs b/src/Propellor/Info.hs
--- a/src/Propellor/Info.hs
+++ b/src/Propellor/Info.hs
@@ -43,11 +43,15 @@
 -- problems with CNAMEs, and also means that when multiple hosts have the
 -- same alias, a DNS round-robin is automatically set up.
 alias :: Domain -> Property
-alias = addDNS . CNAME . AbsDomain
+alias d = pureInfoProperty ("alias " ++ d) $ mempty
+	{ _aliases = S.singleton d
+	-- A CNAME is added here, but the DNS setup code converts it to an
+	-- IP address when that makes sense.
+	, _dns = S.singleton $ CNAME $ AbsDomain d
+	} 
 
 addDNS :: Record -> Property
-addDNS r = pureInfoProperty (rdesc r) $
-	mempty { _dns = S.singleton r }
+addDNS r = pureInfoProperty (rdesc r) $ mempty { _dns = S.singleton r }
   where
 	rdesc (CNAME d) = unwords ["alias", ddesc d]
 	rdesc (Address (IPv4 addr)) = unwords ["ipv4", addr]
@@ -71,8 +75,15 @@
 hostMap :: [Host] -> M.Map HostName Host
 hostMap l = M.fromList $ zip (map hostName l) l 
 
+aliasMap :: [Host] -> M.Map HostName Host
+aliasMap = M.fromList . concat .
+	map (\h -> map (\aka -> (aka, h)) $ S.toList $ _aliases $ hostInfo h)
+
 findHost :: [Host] -> HostName -> Maybe Host
-findHost l hn = M.lookup hn (hostMap l)
+findHost l hn = maybe (findAlias l hn) Just (M.lookup hn (hostMap l))
+
+findAlias :: [Host] -> HostName -> Maybe Host
+findAlias l hn = M.lookup hn (aliasMap l)
 
 getAddresses :: Info -> [IPAddr]
 getAddresses = mapMaybe getIPAddr . S.toList . _dns
diff --git a/src/Propellor/Property/Apache.hs b/src/Propellor/Property/Apache.hs
--- a/src/Propellor/Property/Apache.hs
+++ b/src/Propellor/Property/Apache.hs
@@ -10,20 +10,21 @@
 siteEnabled :: HostName -> ConfigFile -> RevertableProperty
 siteEnabled hn cf = RevertableProperty enable disable
   where
-	enable = trivial $ cmdProperty "a2ensite" ["--quiet", hn]
+	enable = trivial (cmdProperty "a2ensite" ["--quiet", hn])
 		`describe` ("apache site enabled " ++ hn)
 		`requires` siteAvailable hn cf
 		`requires` installed
 		`onChange` reloaded
-	disable = trivial $ File.notPresent (siteCfg hn)
-		`describe` ("apache site disabled " ++ hn)
+	disable = trivial $ combineProperties
+		("apache site disabled " ++ hn) 
+		(map File.notPresent (siteCfg hn))
 		`onChange` cmdProperty "a2dissite" ["--quiet", hn]
 		`requires` installed
 		`onChange` reloaded
 
 siteAvailable :: HostName -> ConfigFile -> Property
-siteAvailable hn cf = siteCfg hn `File.hasContent` (comment:cf)
-	`describe` ("apache site available " ++ hn)
+siteAvailable hn cf = combineProperties ("apache site available " ++ hn) $
+	map (`File.hasContent` (comment:cf)) (siteCfg hn)
   where
 	comment = "# deployed with propellor, do not modify"
 
@@ -39,8 +40,15 @@
 		`requires` installed
 		`onChange` reloaded
 
-siteCfg :: HostName -> FilePath
-siteCfg hn = "/etc/apache2/sites-available/" ++ hn
+-- This is a list of config files because different versions of apache
+-- use different filenames. Propellor simply writen them all.
+siteCfg :: HostName -> [FilePath]
+siteCfg hn =
+	-- Debian pre-2.4
+	[ "/etc/apache2/sites-available/" ++ hn
+	-- Debian 2.4+
+	, "/etc/apache2/sites-available/" ++ hn ++ ".conf"
+	] 
 
 installed :: Property
 installed = Apt.installed ["apache2"]
@@ -60,3 +68,19 @@
 	]
 	`describe` "apache SNI enabled"
 	`onChange` reloaded
+
+-- | Config file fragment that can be inserted into a <Directory>
+-- stanza to allow global read access to the directory.
+--
+-- Works with multiple versions of apache that have different ways to do
+-- it.
+allowAll :: String
+allowAll = unlines
+	[ "<IfVersion < 2.4>"
+	, "Order allow,deny"
+	, "allow from all"
+	, "</IfVersion>"
+	, "<IfVersion >= 2.4>"
+	, "Require all granted"
+	, "</IfVersion>"
+	]
diff --git a/src/Propellor/Property/Cron.hs b/src/Propellor/Property/Cron.hs
--- a/src/Propellor/Property/Cron.hs
+++ b/src/Propellor/Property/Cron.hs
@@ -4,6 +4,7 @@
 import qualified Propellor.Property.File as File
 import qualified Propellor.Property.Apt as Apt
 import Utility.SafeCommand
+import Utility.FileMode
 
 import Data.Char
 
@@ -19,22 +20,33 @@
 --
 -- The cron job's output will only be emailed if it exits nonzero.
 job :: Desc -> CronTimes -> UserName -> FilePath -> String -> Property
-job desc times user cddir command = cronjobfile `File.hasContent`
-	[ "# Generated by propellor"
-	, ""
-	, "SHELL=/bin/sh"
-	, "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
-	, ""
-	, times ++ "\t" ++ user ++ "\t"
-		++ "chronic flock -n " ++ shellEscape cronjobfile
-		++ " sh -c " ++ shellEscape cmdline
+job desc times user cddir command = combineProperties ("cronned " ++ desc)
+	[ cronjobfile `File.hasContent`
+		[ "# Generated by propellor"
+		, ""
+		, "SHELL=/bin/sh"
+		, "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
+		, ""
+		, times ++ "\t" ++ user ++ "\tchronic " ++ shellEscape scriptfile
+		]
+	-- Use a separate script because it makes the cron job name 
+	-- prettier in emails, and also allows running the job manually.
+	, scriptfile `File.hasContent`
+		[ "#!/bin/sh"
+		, "# Generated by propellor"
+		, "set -e"
+		, "flock -n " ++ shellEscape cronjobfile
+			++ " sh -c " ++ shellEscape cmdline
+		]
+	, scriptfile `File.mode` combineModes (readModes ++ executeModes)
 	]
 	`requires` Apt.serviceInstalledRunning "cron"
 	`requires` Apt.installed ["util-linux", "moreutils"]
-	`describe` ("cronned " ++ desc)
   where
 	cmdline = "cd " ++ cddir ++ " && ( " ++ command ++ " )"
-	cronjobfile = "/etc/cron.d/" ++ map sanitize desc
+	cronjobfile = "/etc/cron.d/" ++ name
+	scriptfile = "/usr/local/bin/" ++ name ++ "_cronjob"
+	name = map sanitize desc
 	sanitize c
 		| isAlphaNum c = c
 		| otherwise = '_'
@@ -42,7 +54,7 @@
 -- | Installs a cron job, and runs it niced and ioniced.
 niceJob :: Desc -> CronTimes -> UserName -> FilePath -> String -> Property
 niceJob desc times user cddir command = job desc times user cddir
-	("nice ionice -c 3 " ++ command)
+	("nice ionice -c 3 sh -c " ++ shellEscape command)
 
 -- | Installs a cron job to run propellor.
 runPropellor :: CronTimes -> Property
diff --git a/src/Propellor/Property/File.hs b/src/Propellor/Property/File.hs
--- a/src/Propellor/Property/File.hs
+++ b/src/Propellor/Property/File.hs
@@ -18,28 +18,32 @@
 -- The file's permissions are preserved if the file already existed.
 -- Otherwise, they're set to 600.
 hasPrivContent :: FilePath -> Context -> Property
-hasPrivContent f context = withPrivData (PrivFile f) context $ \getcontent -> 
-	property desc $ getcontent $ \privcontent -> 
-		ensureProperty $ fileProperty' writeFileProtected desc
-			(\_oldcontent -> lines privcontent) f
-  where
-	desc = "privcontent " ++ f
+hasPrivContent = hasPrivContent' writeFileProtected
 
--- | Leaves the file world-readable.
+-- | Leaves the file at its default or current mode,
+-- allowing "private" data to be read.
+--
+-- Use with caution!
 hasPrivContentExposed :: FilePath -> Context -> Property
-hasPrivContentExposed f context = hasPrivContent f context `onChange`
-	mode f (combineModes (ownerWriteMode:readModes))
+hasPrivContentExposed = hasPrivContent' writeFile
 
+hasPrivContent' :: (String -> FilePath -> IO ()) -> FilePath -> Context -> Property
+hasPrivContent' writer f context = 
+	withPrivData (PrivFile f) context $ \getcontent -> 
+		property desc $ getcontent $ \privcontent -> 
+			ensureProperty $ fileProperty' writer desc
+				(\_oldcontent -> lines privcontent) f
+  where
+	desc = "privcontent " ++ f
+
 -- | Ensures that a line is present in a file, adding it to the end if not.
 containsLine :: FilePath -> Line -> Property
 f `containsLine` l = f `containsLines` [l]
 
 containsLines :: FilePath -> [Line] -> Property
-f `containsLines` l = fileProperty (f ++ " contains:" ++ show l) go f
+f `containsLines` ls = fileProperty (f ++ " contains:" ++ show ls) go f
   where
-	go ls
-		| all (`elem` ls) l = ls
-		| otherwise = ls++l
+	go content = content ++ filter (`notElem` content) ls
 
 -- | Ensures that a line is not present in a file.
 -- Note that the file is ensured to exist, so if it doesn't, an empty
diff --git a/src/Propellor/Property/Hostname.hs b/src/Propellor/Property/Hostname.hs
--- a/src/Propellor/Property/Hostname.hs
+++ b/src/Propellor/Property/Hostname.hs
@@ -3,10 +3,14 @@
 import Propellor
 import qualified Propellor.Property.File as File
 
+import Data.List
+
 -- | Ensures that the hostname is set using best practices.
 --
 -- Configures /etc/hostname and the current hostname.
 --
+-- Configures /etc/mailname with the domain part of the hostname.
+--
 -- /etc/hosts is also configured, with an entry for 127.0.1.1, which is
 -- standard at least on Debian to set the FDQN.
 --
@@ -29,6 +33,8 @@
 			else Just $ trivial $ hostsline "127.0.1.1" [hn, basehost]
 		, Just $ trivial $ hostsline "127.0.0.1" ["localhost"]
 		, Just $ trivial $ cmdProperty "hostname" [basehost]
+		, Just $ "/etc/mailname" `File.hasContent`
+			[if null domain then hn else domain]
 		]
 	
 	hostsline ip names = File.fileProperty desc
@@ -37,3 +43,21 @@
 	addhostsline ip names ls =
 		(ip ++ "\t" ++ (unwords names)) : filter (not . hasip ip) ls
 	hasip ip l = headMaybe (words l) == Just ip
+
+-- | Makes /etc/resolv.conf contain search and domain lines for 
+-- the domain that the hostname is in.
+searchDomain :: Property
+searchDomain = property desc (ensureProperty . go =<< asks hostName)
+  where
+	desc = "resolv.conf search and domain configured"
+	go hn =
+		let (_basehost, domain) = separate (== '.') hn
+		in  File.fileProperty desc (use domain) "/etc/resolv.conf"
+	use domain ls = filter wanted $ nub (ls ++ cfgs)
+	  where
+		cfgs = ["domain " ++ domain, "search " ++ domain]
+		wanted l
+			| l `elem` cfgs = True
+			| "domain " `isPrefixOf` l = False
+			| "search " `isPrefixOf` l = False
+			| otherwise = True
diff --git a/src/Propellor/Property/Obnam.hs b/src/Propellor/Property/Obnam.hs
--- a/src/Propellor/Property/Obnam.hs
+++ b/src/Propellor/Property/Obnam.hs
@@ -33,8 +33,8 @@
 -- >		[ "--repository=sftp://2318@usw-s002.rsync.net/~/mygitrepos.obnam"
 -- >		, "--encrypt-with=1B169BE1"
 -- >		] Obnam.OnlyClient
--- >		`requires` Gpg.keyImported "1B169BE1" "root"
--- >		`requires` Ssh.keyImported SshRsa "root"
+-- >		`requires` Gpg.keyImported "1B169BE1" "root" 
+-- >		`requires` Ssh.keyImported SshRsa "root" (Context hostname)
 --
 -- How awesome is that?
 backup :: FilePath -> Cron.CronTimes -> [ObnamParam] -> NumClients -> Property
diff --git a/src/Propellor/Property/Postfix.hs b/src/Propellor/Property/Postfix.hs
--- a/src/Propellor/Property/Postfix.hs
+++ b/src/Propellor/Property/Postfix.hs
@@ -2,24 +2,120 @@
 
 import Propellor
 import qualified Propellor.Property.Apt as Apt
+import Propellor.Property.File
+import qualified Propellor.Property.Service as Service
 
+import qualified Data.Map as M
+import Data.List
+import Data.Char
+
 installed :: Property
 installed = Apt.serviceInstalledRunning "postfix"
 
+restarted :: Property
+restarted = Service.restarted "postfix"
+
+reloaded :: Property
+reloaded = Service.reloaded "postfix"
+
 -- | Configures postfix as a satellite system, which 
--- relats all mail through a relay host, which defaults to smtp.domain. 
+-- relays all mail through a relay host, which defaults to smtp.domain. 
 --
 -- The smarthost may refuse to relay mail on to other domains, without
 -- futher coniguration/keys. But this should be enough to get cron job
 -- mail flowing to a place where it will be seen.
 satellite :: Property
-satellite = setup `requires` installed
+satellite = check (not <$> mainCfIsSet "relayhost") setup
+	`requires` installed
   where
 	setup = trivial $ property "postfix satellite system" $ do
 		hn <- asks hostName
-		ensureProperty $ Apt.reConfigure "postfix"
-			[ ("postfix/main_mailer_type", "select", "Satellite system")
-			, ("postfix/root_address", "string", "root")
-			, ("postfix/destinations", "string", " ")
-			, ("postfix/mailname", "string", hn)
+		let (_, domain) = separate (== '.') hn
+		ensureProperties 
+			[ Apt.reConfigure "postfix"
+				[ ("postfix/main_mailer_type", "select", "Satellite system")
+				, ("postfix/root_address", "string", "root")
+				, ("postfix/destinations", "string", " ")
+				, ("postfix/mailname", "string", hn)
+				]
+			, mainCf ("relayhost", domain)
+				`onChange` reloaded
 			]
+
+-- | Sets up a file by running a property (which the filename is passed
+-- to). If the setup property makes a change, postmap will be run on the
+-- file, and postfix will be reloaded.
+mappedFile :: FilePath -> (FilePath -> Property) -> Property
+mappedFile f setup = setup f
+	`onChange` cmdProperty "postmap" [f]
+
+-- | Run newaliases command, which should be done after changing
+-- /etc/aliases.
+newaliases :: Property
+newaliases = trivial $ cmdProperty "newaliases" []
+
+-- | The main config file for postfix.
+mainCfFile :: FilePath
+mainCfFile = "/etc/postfix/main.cf"
+
+-- | Sets a main.cf name=value pair. Does not reload postfix immediately.
+mainCf :: (String, String) -> Property
+mainCf (name, value) = check notset set
+		`describe` ("postfix main.cf " ++ setting)
+  where
+	setting = name ++ "=" ++ value
+	notset = (/= Just value) <$> getMainCf name
+	set = cmdProperty "postconf" ["-e", setting]
+
+-- | Gets a man.cf setting.
+getMainCf :: String -> IO (Maybe String)
+getMainCf name = parse . lines <$> readProcess "postconf" [name]
+  where
+	parse (l:_) = Just $ 
+		case separate (== '=') l of
+			(_, (' ':v)) -> v
+			(_, v) -> v
+	parse [] = Nothing
+
+-- | Checks if a main.cf field is set. A field that is set to "" 
+-- is considered not set.
+mainCfIsSet :: String -> IO Bool
+mainCfIsSet name = do
+	v <- getMainCf name
+	return $ v /= Nothing && v /= Just ""
+
+-- | Parses main.cf, and removes any initial configuration lines that are
+-- overridden to other values later in the file.
+--
+-- For example, to add some settings, removing any old settings:
+--
+-- > mainCf `File.containsLines`
+-- >	[ "# I like bars."
+-- >	, "foo = bar"
+-- >	] `onChange` dedupMainCf
+--
+-- Note that multiline configurations that continue onto the next line
+-- are not currently supported.
+dedupMainCf :: Property
+dedupMainCf = fileProperty "postfix main.cf dedupped" dedupCf mainCfFile
+
+dedupCf :: [String] -> [String]
+dedupCf ls =
+	let parsed = map parse ls
+	in dedup [] (keycounts $ rights parsed) parsed
+  where	
+	parse l
+		| "#" `isPrefixOf` l = Left l
+		| "=" `isInfixOf` l = 
+			let (k, v) = separate (== '=') l
+			in Right ((filter (not . isSpace) k), v)
+		| otherwise = Left l
+	fmt k v = k ++ " =" ++ v
+
+	keycounts = M.fromListWith (+) . map (\(k, _v) -> (k, (1 :: Integer)))
+
+	dedup c _ [] = reverse c
+	dedup c kc ((Left v):rest) = dedup (v:c) kc rest
+	dedup c kc ((Right (k, v)):rest) = case M.lookup k kc of
+		Just n | n > 1 -> dedup c (M.insert k (n - 1) kc) rest
+		_ -> dedup (fmt k v:c) kc rest
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
@@ -14,12 +14,14 @@
 import qualified Propellor.Property.User as User
 import qualified Propellor.Property.Obnam as Obnam
 import qualified Propellor.Property.Apache as Apache
+import qualified Propellor.Property.Postfix as Postfix
 import Utility.SafeCommand
 import Utility.FileMode
 import Utility.Path
 
 import Data.List
 import System.Posix.Files
+import Data.String.Utils
 
 oldUseNetServer :: [Host] -> Property
 oldUseNetServer hosts = propertyList ("olduse.net server")
@@ -59,9 +61,7 @@
 		, "  <Directory " ++ datadir ++ "/>"
 		, "    Options Indexes FollowSymlinks"
 		, "    AllowOverride None"
-		-- I had this in the file before.
-		-- This may be needed by a newer version of apache?
-		--, "    Require all granted"
+		, Apache.allowAll
 		, "  </Directory>"
 		]
 	]
@@ -114,11 +114,11 @@
 	[ Apt.serviceInstalledRunning "mumble-server"
 	, Obnam.latestVersion
 	, Obnam.backup "/var/lib/mumble-server" "55 5 * * *"
-		[ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/" ++ hn ++ ".obnam"
+		[ "--repository=sftp://joey@usbackup.kitenet.net/~/lib/backup/" ++ hn ++ ".obnam"
 		, "--client-name=mumble"
 		] Obnam.OnlyClient
 		`requires` Ssh.keyImported SshRsa "root" (Context hn)
-		`requires` Ssh.knownHost hosts "turtle.kitenet.net" "root"
+		`requires` Ssh.knownHost hosts "usbackup.kitenet.net" "root"
 	, trivial $ cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"]
 	]
   where
@@ -142,7 +142,7 @@
 	, Obnam.backup "/srv/git" "33 3 * * *"
 		[ "--repository=sftp://2318@usw-s002.rsync.net/~/git.kitenet.net"
 		, "--encrypt-with=1B169BE1"
-		, "--client-name=wren"
+		, "--client-name=wren" -- historical
 		] Obnam.OnlyClient
 		`requires` Gpg.keyImported "1B169BE1" "root"
 		`requires` Ssh.keyImported SshRsa "root" (Context "git.kitenet.net")
@@ -191,8 +191,8 @@
 type AnnexUUID = String
 
 -- | A website, with files coming from a git-annex repository.
-annexWebSite :: [Host] -> Git.RepoUrl -> HostName -> AnnexUUID -> [(String, Git.RepoUrl)] -> Property
-annexWebSite hosts origin hn uuid remotes = propertyList (hn ++" website using git-annex")
+annexWebSite :: Git.RepoUrl -> HostName -> AnnexUUID -> [(String, Git.RepoUrl)] -> Property
+annexWebSite origin hn uuid remotes = propertyList (hn ++" website using git-annex")
 	[ Git.cloned "joey" origin dir Nothing
 		`onChange` setup
 	, postupdatehook `File.hasContent`
@@ -206,8 +206,6 @@
 	dir = "/srv/web/" ++ hn
 	postupdatehook = dir </> ".git/hooks/post-update"
 	setup = userScriptProperty "joey" setupscript
-		`requires` Ssh.keyImported SshRsa "joey" (Context hn)
-		`requires` Ssh.knownHost hosts "turtle.kitenet.net" "joey"
 	setupscript = 
 		[ "cd " ++ shellEscape dir
 		, "git config annex.uuid " ++ shellEscape uuid
@@ -348,8 +346,27 @@
 	, let f = "/home/joey/.github-keys"
 	  in File.hasPrivContent f anyContext
 		`onChange` File.ownerGroup f "joey" "joey"
+	, Cron.niceJob "github-backup run" "30 4 * * *" "joey"
+		"/home/joey/lib/backup" $ intercalate "&&"
+			[ "mkdir -p github"
+			, "cd github"
+			, ". $HOME/.github-keys && github-backup joeyh"
+			]
 	]
 
+rsyncNetBackup :: [Host] -> Property
+rsyncNetBackup hosts = Cron.niceJob "rsync.net copied in daily" "30 5 * * *"
+	"joey" "/home/joey/lib/backup" "mkdir -p rsync.net && rsync --delete -az 2318@usw-s002.rsync.net: rsync.net"
+	`requires` Ssh.knownHost hosts "usw-s002.rsync.net" "joey"
+
+backupsBackedupTo :: [Host] -> HostName -> FilePath -> Property
+backupsBackedupTo hosts desthost destdir = Cron.niceJob desc
+	"1 1 * * 3" "joey" "/" cmd
+	`requires` Ssh.knownHost hosts desthost "joey"
+  where
+	desc = "backups copied to " ++ desthost ++ " weekly"
+	cmd = "rsync -az --delete /home/joey/lib/backup " ++ desthost ++ ":" ++ destdir
+
 obnamRepos :: [String] -> Property
 obnamRepos rs = propertyList ("obnam repos for " ++ unwords rs)
 	(mkbase : map mkrepo rs)
@@ -360,3 +377,354 @@
 	mkdir d = File.dirExists d
 		`before` File.ownerGroup d "joey" "joey"
 
+podcatcher :: Property
+podcatcher = Cron.niceJob "podcatcher run hourly" "55 * * * *"
+	"joey" "/home/joey/lib/sound/podcasts"
+	"xargs git-annex importfeed -c annex.genmetadata=true < feeds; mr --quiet update"
+	`requires` Apt.installed ["git-annex", "myrepos"]
+
+kiteMailServer :: Property
+kiteMailServer = propertyList "kitenet.net mail server"
+	[ Postfix.installed
+	, Apt.installed ["postfix-pcre"]
+	, Apt.serviceInstalledRunning "postgrey"
+
+	, Apt.serviceInstalledRunning "spamassassin"
+	, "/etc/default/spamassassin" `File.containsLines`
+		[ "# Propellor deployed"
+		, "ENABLED=1"
+		, "CRON=1"
+		, "OPTIONS=\"--create-prefs --max-children 5 --helper-home-dir\""
+		, "CRON=1"
+		, "NICE=\"--nicelevel 15\""
+		] `onChange` Service.restarted "spamassassin"
+		`describe` "spamd enabled"
+		`requires` Apt.serviceInstalledRunning "cron"
+	
+	, Apt.serviceInstalledRunning "spamass-milter"
+	-- Add -m to prevent modifying messages Subject or body.
+	, "/etc/default/spamass-milter" `File.containsLine`
+		"OPTIONS=\"-m -u spamass-milter -i 127.0.0.1\""
+		`onChange` Service.restarted "spamass-milter"
+		`describe` "spamass-milter configured"
+	
+	, Apt.serviceInstalledRunning "amavisd-milter"
+	, "/etc/default/amavisd-milter" `File.containsLines`
+		[ "# Propellor deployed"
+		, "MILTERSOCKET=/var/spool/postfix/amavis/amavis.sock"
+		, "MILTERSOCKETOWNER=\"postfix:postfix\""
+		, "MILTERSOCKETMODE=\"0660\""
+		]
+		`onChange` Service.restarted "amavisd-milter"
+		`describe` "amavisd-milter configured for postfix"
+	, Apt.serviceInstalledRunning "clamav-freshclam"
+
+	, Apt.installed ["maildrop"]
+	, "/etc/maildroprc" `File.hasContent`
+		[ "# Global maildrop filter file (deployed with propellor)"
+		, "DEFAULT=\"$HOME/Maildir\""
+		, "MAILBOX=\"$DEFAULT/.\""
+		, "# Filter spam to a spam folder, unless .keepspam exists"
+		, "if (/^X-Spam-Status: Yes/)"
+		, "{"
+		, "  `test -e \"$HOME/.keepspam\"`"
+		, "  if ( $RETURNCODE != 0 )"
+		, "  to ${MAILBOX}spam"
+		, "}"
+		]
+		`describe` "maildrop configured"
+
+	, "/etc/aliases" `File.hasPrivContentExposed` ctx
+		`onChange` Postfix.newaliases
+	, hasJoeyCAChain
+	, "/etc/ssl/certs/postfix.pem" `File.hasPrivContentExposed` ctx
+	, "/etc/ssl/private/postfix.pem" `File.hasPrivContent` ctx
+
+	, "/etc/postfix/mydomain" `File.containsLines`
+		[ "/.*\\.kitenet\\.net/\tOK"
+		, "/ikiwiki\\.info/\tOK"
+		, "/joeyh\\.name/\tOK"
+		]
+		`onChange` Postfix.reloaded
+		`describe` "postfix mydomain file configured"
+	, "/etc/postfix/obscure_client_relay.pcre" `File.containsLine`
+		"/^Received: from ([^.]+)\\.kitenet\\.net.*using TLS.*by kitenet\\.net \\(([^)]+)\\) with (E?SMTPS?A?) id ([A-F[:digit:]]+)(.*)/ IGNORE"
+		`onChange` Postfix.reloaded
+		`describe` "postfix obscure_client_relay file configured"
+	, Postfix.mappedFile "/etc/postfix/virtual"
+		(flip File.containsLines
+			[ "# *@joeyh.name to joey"
+			, "@joeyh.name\tjoey"
+			]
+		) `describe` "postfix virtual file configured"
+		`onChange` Postfix.reloaded
+	, Postfix.mappedFile "/etc/postfix/relay_clientcerts" $
+		flip File.hasPrivContentExposed ctx
+	, Postfix.mainCfFile `File.containsLines`
+		[ "myhostname = kitenet.net"
+		, "mydomain = $myhostname"
+		, "append_dot_mydomain = no"
+		, "myorigin = kitenet.net"
+		, "mydestination = $myhostname, localhost.$mydomain, $mydomain, kite.$mydomain., localhost, regexp:$config_directory/mydomain"
+		, "mailbox_command = maildrop"
+		, "virtual_alias_maps = hash:/etc/postfix/virtual"
+
+		, "# Allow clients with trusted certs to relay mail through."
+		, "relay_clientcerts = hash:/etc/postfix/relay_clientcerts"
+		, "smtpd_relay_restrictions = permit_mynetworks,permit_tls_clientcerts,permit_sasl_authenticated,reject_unauth_destination"
+
+		, "# Filter out client relay lines from headers."
+		, "header_checks = pcre:$config_directory/obscure_client_relay.pcre"
+
+		, "# Enable postgrey."
+		, "smtpd_recipient_restrictions = permit_mynetworks,reject_unauth_destination,check_policy_service inet:127.0.0.1:10023"
+
+		, "# Enable spamass-milter and amavis-milter."
+		, "smtpd_milters = unix:/spamass/spamass.sock unix:amavis/amavis.sock"
+		, "milter_connect_macros = j {daemon_name} v {if_name} _"
+
+		, "# TLS setup -- server"
+		, "smtpd_tls_CAfile = /etc/ssl/certs/joeyca.pem"
+		, "smtpd_tls_cert_file = /etc/ssl/certs/postfix.pem"
+		, "smtpd_tls_key_file = /etc/ssl/private/postfix.pem"
+		, "smtpd_tls_loglevel = 1"
+		, "smtpd_tls_received_header = yes"
+		, "smtpd_use_tls = yes"
+		, "smtpd_tls_ask_ccert = yes"
+		, "smtpd_tls_session_cache_database = sdbm:/etc/postfix/smtpd_scache"
+
+		, "# TLS setup -- client"
+		, "smtp_tls_CAfile = /etc/ssl/certs/joeyca.pem"
+		, "smtp_tls_cert_file = /etc/ssl/certs/postfix.pem"
+		, "smtp_tls_key_file = /etc/ssl/private/postfix.pem"
+		, "smtp_tls_loglevel = 1"
+		, "smtp_use_tls = yes"
+		, "smtp_tls_session_cache_database = sdbm:/etc/postfix/smtp_scache"
+		]
+		`onChange` Postfix.dedupMainCf
+		`onChange` Postfix.reloaded
+		`describe` "postfix configured"
+	
+	, Apt.serviceInstalledRunning "dovecot-imapd"
+	, Apt.serviceInstalledRunning "dovecot-pop3d"
+	, "/etc/dovecot/conf.d/10-mail.conf" `File.containsLine`
+		"mail_location = maildir:~/Maildir"
+		`onChange` Service.reloaded "dovecot"
+		`describe` "dovecot mail.conf"
+	, "/etc/dovecot/conf.d/10-auth.conf" `File.containsLine`
+		"!include auth-passwdfile.conf.ext"
+		`onChange` Service.restarted "dovecot"
+		`describe` "dovecot auth.conf"
+	, File.hasPrivContent dovecotusers ctx
+		`onChange` (dovecotusers `File.mode`
+			combineModes [ownerReadMode, groupReadMode])
+	, File.ownerGroup dovecotusers "root" "dovecot"
+
+	, Apt.installed ["mutt", "bsd-mailx", "alpine"]
+
+	, pinescript `File.hasContent`
+		[ "#!/bin/sh"
+		, "# deployed with propellor"
+		, "set -e"
+		, "pass=$HOME/.pine-password"
+		, "if [ ! -e $pass ]; then"
+		, "\ttouch $pass"
+		, "fi"
+		, "chmod 600 $pass"
+		, "exec alpine -passfile $pass \"$@\""
+		]
+		`onChange` (pinescript `File.mode`
+			combineModes (readModes ++ executeModes))
+		`describe` "pine wrapper script"
+	, "/etc/pine.conf" `File.containsLines`
+		[ "inbox-path={localhost/novalidate-cert}inbox"
+		]
+		`describe` "pine configured to use local imap server"
+	]
+  where
+	ctx = Context "kitenet.net"
+	pinescript = "/usr/local/bin/pine"
+	dovecotusers = "/etc/dovecot/users"
+
+hasJoeyCAChain :: Property
+hasJoeyCAChain = "/etc/ssl/certs/joeyca.pem" `File.hasPrivContentExposed`
+	Context "joeyca.pem"
+
+kitenetHttps :: Property
+kitenetHttps = propertyList "kitenet.net https certs"
+	[ File.hasPrivContent "/etc/ssl/certs/web.pem" ctx
+	, File.hasPrivContent "/etc/ssl/private/web.pem" ctx
+	, File.hasPrivContent "/etc/ssl/certs/startssl.pem" ctx
+	, toProp $ Apache.modEnabled "ssl"
+	]
+  where
+	ctx = Context "kitenet.net"
+
+-- Legacy static web sites and redirections from kitenet.net to newer
+-- sites.
+legacyWebSites :: Property
+legacyWebSites = propertyList "legacy web sites"
+	[ Apt.serviceInstalledRunning "apache2"
+	, toProp $ Apache.modEnabled "rewrite"
+	, toProp $ Apache.modEnabled "cgi"
+	, toProp $ Apache.modEnabled "speling"
+	, userDirHtml
+	, kitenetHttps
+	, toProp $ Apache.siteEnabled "kitenet.net" $ apachecfg "kitenet.net" True
+		-- /var/www is empty
+		[ "DocumentRoot /var/www"
+		, "<Directory /var/www>"
+		, "  Options Indexes FollowSymLinks MultiViews ExecCGI Includes"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+		, "ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/"
+
+		-- for mailman cgi scripts
+		, "<Directory /usr/lib/cgi-bin>"
+		, "  AllowOverride None"
+		, "  Options ExecCGI"
+		, Apache.allowAll
+		, "</Directory>"
+		, "Alias /pipermail/ /var/lib/mailman/archives/public/"
+		, "<Directory /var/lib/mailman/archives/public/>"
+		, "  Options Indexes MultiViews FollowSymlinks"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+		, "Alias /images/ /usr/share/images/"
+		, "<Directory /usr/share/images/>"
+		, "  Options Indexes MultiViews"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+
+		, "RewriteEngine On"
+		, "# Force hostname to kitenet.net"
+		, "RewriteCond %{HTTP_HOST} !^kitenet\\.net [NC]"
+		, "RewriteCond %{HTTP_HOST} !^$"
+		, "RewriteRule ^/(.*) http://kitenet\\.net/$1 [L,R]"
+
+		, "# Moved pages"
+		, "RewriteRule /programs/debhelper http://joeyh.name/code/debhelper/ [L]"
+		, "RewriteRule /programs/satutils http://joeyh.name/code/satutils/ [L]"
+		, "RewriteRule /programs/filters http://joeyh.name/code/filters/ [L]"
+		, "RewriteRule /programs/ticker http://joeyh.name/code/ticker/ [L]"
+		, "RewriteRule /programs/pdmenu http://joeyh.name/code/pdmenu/ [L]"
+		, "RewriteRule /programs/sleepd http://joeyh.name/code/sleepd/ [L]"
+		, "RewriteRule /programs/Lingua::EN::Words2Nums http://joeyh.name/code/Words2Nums/ [L]"
+		, "RewriteRule /programs/wmbattery http://joeyh.name/code/wmbattery/ [L]"
+		, "RewriteRule /programs/dpkg-repack http://joeyh.name/code/dpkg-repack/ [L]"
+		, "RewriteRule /programs/debconf http://joeyh.name/code/debconf/ [L]"
+		, "RewriteRule /programs/perlmoo http://joeyh.name/code/perlmoo/ [L]"
+		, "RewriteRule /programs/alien http://joeyh.name/code/alien/ [L]"
+		, "RewriteRule /~joey/blog/entry/(.+)-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9].html http://joeyh.name/blog/entry/$1/ [L]"
+		, "RewriteRule /~anna/.* http://waldeneffect\\.org/ [R]"
+		, "RewriteRule /~anna/.* http://waldeneffect\\.org/ [R]"
+		, "RewriteRule /~anna http://waldeneffect\\.org/ [R]"
+		, "RewriteRule /simpleid/ http://openid.kitenet.net:8081/simpleid/"
+		, "# Even the kite home page is not here any more!"
+		, "RewriteRule ^/$ http://www.kitenet.net/ [R]"
+		, "RewriteRule ^/index.html http://www.kitenet.net/ [R]"
+		, "RewriteRule ^/joey http://www.kitenet.net/joey/ [R]"
+		, "RewriteRule ^/joey/index.html http://www.kitenet.net/joey/ [R]"
+		, "RewriteRule ^/wifi http://www.kitenet.net/wifi/ [R]"
+		, "RewriteRule ^/wifi/index.html http://www.kitenet.net/wifi/ [R]"
+		
+		, "# Old ikiwiki filenames for kitenet.net wiki."
+		, "rewritecond $1 !^/~"
+		, "rewritecond $1 !^/doc/"
+		, "rewritecond $1 !^/pipermail/"
+		, "rewritecond $1 !^/cgi-bin/"
+		, "rewritecond $1 !.*/index$"
+		, "rewriterule (.+).html$ $1/ [r]"
+
+		, "# Old ikiwiki filenames for joey's wiki."
+		, "rewritecond $1 ^/~joey/"
+		, "rewritecond $1 !.*/index$"
+		, "rewriterule (.+).html$ http://kitenet.net/$1/ [L,R]"
+
+		, "# ~joey to joeyh.name"
+		, "rewriterule /~joey/(.*) http://joeyh.name/$1 [L]"
+
+		, "# Old familywiki location."
+		, "rewriterule /~family/(.*).html http://family.kitenet.net/$1 [L]"
+		, "rewriterule /~family/(.*).rss http://family.kitenet.net/$1/index.rss [L]"
+		, "rewriterule /~family(.*) http://family.kitenet.net$1 [L]"
+
+		, "rewriterule /~kyle/bywayofscience(.*) http://bywayofscience.branchable.com$1 [L]"
+		, "rewriterule /~kyle/family/wiki/(.*).html http://macleawiki.branchable.com/$1 [L]"
+		, "rewriterule /~kyle/family/wiki/(.*).rss http://macleawiki.branchable.com/$1/index.rss [L]"
+		, "rewriterule /~kyle/family/wiki(.*) http://macleawiki.branchable.com$1 [L]"
+		]
+	, alias "anna.kitenet.net"
+	, toProp $ Apache.siteEnabled "anna.kitenet.net" $ apachecfg "anna.kitenet.net" False
+		[ "DocumentRoot /home/anna/html"
+		, "<Directory /home/anna/html/>"
+		, "  Options Indexes ExecCGI"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+		]
+	, alias "sows-ear.kitenet.net"
+	, alias "www.sows-ear.kitenet.net"
+	, toProp $ Apache.siteEnabled "sows-ear.kitenet.net" $ apachecfg "sows-ear.kitenet.net" False
+		[ "ServerAlias www.sows-ear.kitenet.net"
+		, "DocumentRoot /srv/web/sows-ear.kitenet.net"
+		, "<Directory /srv/web/sows-ear.kitenet.net>"
+		, "  Options FollowSymLinks"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+		]
+	, alias "wortroot.kitenet.net"
+	, alias "www.wortroot.kitenet.net"
+	, toProp $ Apache.siteEnabled "wortroot.kitenet.net" $ apachecfg "wortroot.kitenet.net" False
+		[ "ServerAlias www.wortroot.kitenet.net"
+		, "DocumentRoot /srv/web/wortroot.kitenet.net"
+		, "<Directory /srv/web/wortroot.kitenet.net>"
+		, "  Options FollowSymLinks"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+		]
+	, alias "creeksidepress.com"
+	, toProp $ Apache.siteEnabled "creeksidepress.com" $ apachecfg "creeksidepress.com" False
+		[ "ServerAlias www.creeksidepress.com"
+		, "DocumentRoot /srv/web/www.creeksidepress.com"
+		, "<Directory /srv/web/www.creeksidepress.com>"
+		, "  Options FollowSymLinks"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+		]
+	, alias "joey.kitenet.net"
+	, toProp $ Apache.siteEnabled "joey.kitenet.net" $ apachecfg "joey.kitenet.net" False
+		[ "DocumentRoot /home/joey/html"
+		, "<Directory /home/joey/html/>"
+		, "  Options Indexes ExecCGI"
+		, "  AllowOverride None"
+		, Apache.allowAll
+		, "</Directory>"
+
+		, "RewriteEngine On"
+
+		, "# Old ikiwiki filenames for joey's wiki."
+		, "rewritecond $1 !.*/index$"
+		, "rewriterule (.+).html$ http://joeyh.name/$1/ [l]"
+
+		, "rewritecond $1 !.*/index$"
+		, "rewriterule (.+).rss$ http://joeyh.name/$1/index.rss [l]"
+		
+		, "# Redirect all to joeyh.name."
+		, "rewriterule (.*) http://joeyh.name$1 [r]"
+		]
+	]
+
+userDirHtml :: Property
+userDirHtml = File.fileProperty "apache userdir is html" (map munge) conf
+	`onChange` Apache.reloaded
+	`requires` (toProp $ Apache.modEnabled "userdir")
+  where
+	munge = replace "public_html" "html"
+	conf = "/etc/apache2/mods-available/userdir.conf"
diff --git a/src/Propellor/Types/Info.hs b/src/Propellor/Types/Info.hs
--- a/src/Propellor/Types/Info.hs
+++ b/src/Propellor/Types/Info.hs
@@ -12,6 +12,7 @@
 	{ _os :: Val System
 	, _privDataFields :: S.Set (PrivDataField, Context)
 	, _sshPubKey :: Val String
+	, _aliases :: S.Set HostName
 	, _dns :: S.Set Dns.Record
 	, _namedconf :: Dns.NamedConfMap
 	, _dockerinfo :: DockerInfo
@@ -19,11 +20,12 @@
 	deriving (Eq, Show)
 
 instance Monoid Info where
-	mempty = Info mempty mempty mempty mempty mempty mempty
+	mempty = Info mempty mempty mempty mempty mempty mempty mempty
 	mappend old new = Info
 		{ _os = _os old <> _os new
 		, _privDataFields = _privDataFields old <> _privDataFields new
 		, _sshPubKey = _sshPubKey old <> _sshPubKey new
+		, _aliases = _aliases old <> _aliases new
 		, _dns = _dns old <> _dns new
 		, _namedconf = _namedconf old <> _namedconf new
 		, _dockerinfo = _dockerinfo old <> _dockerinfo new
