module Propellor.Property.DnsSec where import Propellor.Base import qualified Propellor.Property.File as File -- | Puts the DNSSEC key files in place from PrivData. -- -- signedPrimary uses this, so this property does not normally need to be -- used directly. keysInstalled :: Domain -> RevertableProperty (HasInfo + UnixLike) UnixLike keysInstalled domain = setup cleanup where setup = propertyList "DNSSEC keys installed" $ toProps $ map installkey keys cleanup = propertyList "DNSSEC keys removed" $ toProps $ map (File.notPresent . keyFn domain) keys installkey k = writer (keysrc k) (keyFn domain k) (Context domain) where writer | isPublic k = File.hasPrivContentExposedFrom | otherwise = File.hasPrivContentFrom keys = [ PubZSK, PrivZSK, PubKSK, PrivKSK ] keysrc k = PrivDataSource (DnsSec k) $ unwords [ "The file with extension" , keyExt k , "created by running:" , if isZoneSigningKey k then "dnssec-keygen -a RSASHA256 -b 2048 -n ZONE " ++ domain else "dnssec-keygen -f KSK -a RSASHA256 -b 4096 -n ZONE " ++ domain ] -- | Uses dnssec-signzone to sign a domain's zone file. -- -- signedPrimary uses this, so this property does not normally need to be -- used directly. zoneSigned :: Domain -> FilePath -> RevertableProperty (HasInfo + UnixLike) UnixLike zoneSigned domain zonefile = setup cleanup where setup :: Property (HasInfo + UnixLike) setup = check needupdate (forceZoneSigned domain zonefile) `requires` keysInstalled domain cleanup :: Property UnixLike cleanup = File.notPresent (signedZoneFile zonefile) `before` File.notPresent dssetfile `before` revert (keysInstalled domain) dssetfile = dir "-" ++ domain ++ "." dir = takeDirectory zonefile -- Need to update the signed zone file if the zone file or -- any of the keys have a newer timestamp. needupdate = do v <- catchMaybeIO $ getModificationTime (signedZoneFile zonefile) case v of Nothing -> return True Just t1 -> anyM (newerthan t1) $ zonefile : map (keyFn domain) [minBound..maxBound] newerthan t1 f = do t2 <- getModificationTime f return (t2 >= t1) forceZoneSigned :: Domain -> FilePath -> Property UnixLike forceZoneSigned domain zonefile = property ("zone signed for " ++ domain) $ liftIO $ do salt <- take 16 <$> saltSha1 let p = proc "dnssec-signzone" [ "-A" , "-3", salt -- The serial number needs to be increased each time the -- zone is resigned, even if there are no other changes, -- so that it will propagate to secondaries. So, use the -- unixtime serial format. , "-N", "unixtime" , "-o", domain , zonefile -- the ordering of these key files does not matter , keyFn domain PubZSK , keyFn domain PubKSK ] -- Run in the same directory as the zonefile, so it will -- write the dsset file there. (_, _, _, h) <- createProcess $ p { cwd = Just (takeDirectory zonefile) } ifM (checkSuccessProcess h) ( return MadeChange , return FailedChange ) saltSha1 :: IO String saltSha1 = readProcess "sh" [ "-c" , "head -c 1024 /dev/urandom | sha1sum | cut -d ' ' -f 1" ] -- | The file used for a given key. keyFn :: Domain -> DnsSecKey -> FilePath keyFn domain k = "/etc/bind/propellor/dnssec" concat [ "K" ++ domain ++ "." , if isZoneSigningKey k then "ZSK" else "KSK" , keyExt k ] -- | These are the extensions that dnssec-keygen looks for. keyExt :: DnsSecKey -> String keyExt k | isPublic k = ".key" | otherwise = ".private" isPublic :: DnsSecKey -> Bool isPublic k = k `elem` [PubZSK, PubKSK] isZoneSigningKey :: DnsSecKey -> Bool isZoneSigningKey k = k `elem` [PubZSK, PrivZSK] -- | dnssec-signzone makes a .signed file signedZoneFile :: FilePath -> FilePath signedZoneFile zonefile = zonefile ++ ".signed"