From b845b1c5efc1362dc78baf87747ba8b90fcd97dd Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Sun, 7 Feb 2016 16:28:48 -0400 Subject: letsencrypt * Added Propellor.Property.LetsEncrypt * Apache.httpsVirtualHost: New property, setting up a https vhost with the certificate automatically obtained using letsencrypt. --- src/Propellor/Property/LetsEncrypt.hs | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/Propellor/Property/LetsEncrypt.hs (limited to 'src/Propellor/Property/LetsEncrypt.hs') diff --git a/src/Propellor/Property/LetsEncrypt.hs b/src/Propellor/Property/LetsEncrypt.hs new file mode 100644 index 00000000..651cffd9 --- /dev/null +++ b/src/Propellor/Property/LetsEncrypt.hs @@ -0,0 +1,115 @@ +-- | 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, and the `CertInstaller` is +-- also run after renewal. 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 + if startstats == endstats + then return NoChange + else ensureProperty certsinstalled + 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 = propertyList ("certs installed") $ + flip map alldomains $ \d -> certinstaller 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. +type CertInstaller = 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" -- cgit v1.2.3