-- | This module uses the letsencrypt reference client. module Propellor.Property.LetsEncrypt where import Propellor.Base import qualified Propellor.Property.Apt as Apt import System.Posix.Files installed :: Property NoInfo installed = Apt.installed ["letsencrypt"] -- | Tell the letsencrypt client that you agree with the Let's Encrypt -- Subscriber Agreement. Providing an email address is recommended, -- so that letcencrypt can contact you about problems. data AgreeTOS = AgreeTOS (Maybe Email) type Email = String type WebRoot = FilePath -- | Uses letsencrypt to obtain a certificate for a domain. -- -- This should work with any web server, as long as letsencrypt can -- write its temp files to the web root. The letsencrypt client does -- not modify the web server's configuration in any way; instead the -- `CertInstaller` is used once the client has successfully obtained the -- certificate. -- -- This also handles renewing the certificate. -- For renewel to work well, propellor needs to be -- run periodically (at least a couple times per month). -- -- See `Propellor.Property.Apache.httpsVirtualHost` for a property built using this. letsEncrypt :: AgreeTOS -> Domain -> WebRoot -> CertInstaller -> Property NoInfo letsEncrypt tos domain = letsEncrypt' tos domain [] -- | Like `letsEncrypt`, but the certificate can be obtained for multiple -- domains. letsEncrypt' :: AgreeTOS -> Domain -> [Domain] -> WebRoot -> CertInstaller -> Property NoInfo letsEncrypt' (AgreeTOS memail) domain domains webroot certinstaller = prop `requires` installed where prop = property desc $ do startstats <- liftIO getstats (transcript, ok) <- liftIO $ processTranscript "letsencrypt" params Nothing if ok then do endstats <- liftIO getstats ensureProperty $ certsinstalled (startstats /= endstats) else do liftIO $ hPutStr stderr transcript return FailedChange desc = "letsencrypt " ++ unwords alldomains alldomains = domain : domains params = [ "certonly" , "--agree-tos" , case memail of Just email -> "--email="++email Nothing -> "--register-unsafely-without-email" , "--webroot" , "--webroot-path", webroot , "--text" , "--keep-until-expiring" ] ++ map (\d -> "--domain="++d) alldomains getstats = mapM statcertfiles alldomains statcertfiles d = mapM statfile [ certFile d , privKeyFile d , chainFile d , fullChainFile d ] statfile f = catchMaybeIO $ do s <- getFileStatus f return (fileID s, deviceID s, fileMode s, fileSize s, modificationTime s) certsinstalled newcert = propertyList ("certs installed") $ flip map alldomains $ \d -> certinstaller newcert d (certFile d) (privKeyFile d) (chainFile d) (fullChainFile d) -- | A property that installs a certificate, once letsencrypt obtains it. -- -- For example, it could configure the web server to use the certificate -- files, and restart the web server. -- -- The Bool is True when a new cerficate was just obtained. -- But, this is also run when the certificate has not changed, so that -- any changes to the property will take effect. type CertInstaller = Bool -> Domain -> CertFile -> PrivKeyFile -> ChainFile -> FullChainFile -> Property NoInfo -- | Locations of certificate files generated by lets encrypt. type CertFile = FilePath type PrivKeyFile = FilePath type ChainFile = FilePath type FullChainFile = FilePath liveCertDir :: Domain -> FilePath liveCertDir d = "/etc/letsencrypt/live" d certFile :: Domain -> FilePath certFile d = liveCertDir d "cert.pem" privKeyFile :: Domain -> FilePath privKeyFile d = liveCertDir d "privkey.pem" chainFile :: Domain -> FilePath chainFile d = liveCertDir d "chain.pem" fullChainFile :: Domain -> FilePath fullChainFile d = liveCertDir d "fullchain.pem"