From efbb3d0e126721e0f9487f194379806c37f1988e Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Sun, 7 Feb 2016 21:57:35 -0400 Subject: rethought how letsencrypt cert change is handled Simply use onChange to let any other property by run after letsencrypt gets/renews the cert. Much simpler and allows for revertable properties too! --- src/Propellor/Property/Apache.hs | 21 +++++++-------- src/Propellor/Property/LetsEncrypt.hs | 51 +++++++++++++---------------------- 2 files changed, 28 insertions(+), 44 deletions(-) (limited to 'src/Propellor') diff --git a/src/Propellor/Property/Apache.hs b/src/Propellor/Property/Apache.hs index c2c32a3b..d0bcadfa 100644 --- a/src/Propellor/Property/Apache.hs +++ b/src/Propellor/Property/Apache.hs @@ -161,12 +161,12 @@ httpsVirtualHost domain docroot letos = httpsVirtualHost' domain docroot letos [ -- | Like `httpsVirtualHost` but with additional config lines added. httpsVirtualHost' :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> [ConfigLine] -> Property NoInfo -httpsVirtualHost' domain docroot letos addedcfg = setup +httpsVirtualHost' domain docroot letos addedcfg = setuphttp `requires` modEnabled "rewrite" `requires` modEnabled "ssl" - `before` LetsEncrypt.letsEncrypt letos domain docroot certinstaller + `before` setuphttps where - setup = siteEnabled' domain $ + setuphttp = siteEnabled' domain $ -- The sslconffile is only created after letsencrypt gets -- the cert. The "*" is needed to make apache not error -- when the file doesn't exist. @@ -179,22 +179,21 @@ httpsVirtualHost' domain docroot letos addedcfg = setup -- Everything else redirects to https , "RewriteRule ^/(.*) https://" ++ domain ++ "/$1 [L,R,NE]" ] - certinstaller :: LetsEncrypt.CertInstaller - certinstaller newcert _domain certfile privkeyfile chainfile _fullchainfile = - combineProperties (domain ++ " ssl cert installed") + setuphttps = LetsEncrypt.letsEncrypt letos domain docroot + `onChange` combineProperties (domain ++ " ssl cert installed") [ File.dirExists (takeDirectory cf) , File.hasContent cf sslvhost `onChange` reloaded - -- always reload when the cert has changed - , check (return newcert :: IO Bool) reloaded + -- always reload since the cert has changed + , reloaded ] where cf = sslconffile "letsencrypt" sslvhost = vhost (Port 443) [ "SSLEngine on" - , "SSLCertificateFile " ++ certfile - , "SSLCertificateKeyFile " ++ privkeyfile - , "SSLCertificateChainFile " ++ chainfile + , "SSLCertificateFile " ++ LetsEncrypt.certFile domain + , "SSLCertificateKeyFile " ++ LetsEncrypt.privKeyFile domain + , "SSLCertificateChainFile " ++ LetsEncrypt.chainFile domain ] sslconffile s = "/etc/apache2/sites-available/ssl/" ++ domain ++ "/" ++ s ++ ".conf" vhost (Port p) ls = diff --git a/src/Propellor/Property/LetsEncrypt.hs b/src/Propellor/Property/LetsEncrypt.hs index 2df290be..d0dbc4e7 100644 --- a/src/Propellor/Property/LetsEncrypt.hs +++ b/src/Propellor/Property/LetsEncrypt.hs @@ -23,22 +23,29 @@ type WebRoot = FilePath -- -- 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. +-- not modify the web server's configuration in any way; this only obtains +-- the certificate it does not make the web server use it. -- -- 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 +-- This property returns `MadeChange` when the certificate is initially +-- obtained, and when it's renewed. So, it can be combined with a property +-- to make the webserver (or other server) use the certificate: +-- +-- > letsEncrypt (AgreeTOS (Just "me@example.com")) "example.com" "/var/www" +-- > `onChange` Apache.reload +-- +-- See `Propellor.Property.Apache.httpsVirtualHost` for a simpler way to +-- use letsencrypt, that is built on top of this. +letsEncrypt :: AgreeTOS -> Domain -> WebRoot -> 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 = +letsEncrypt' :: AgreeTOS -> Domain -> [Domain] -> WebRoot -> Property NoInfo +letsEncrypt' (AgreeTOS memail) domain domains webroot = prop `requires` installed where prop = property desc $ do @@ -48,8 +55,9 @@ letsEncrypt' (AgreeTOS memail) domain domains webroot certinstaller = if ok then do endstats <- liftIO getstats - ensureProperty $ - certsinstalled (startstats /= endstats) + if startstats /= endstats + then return MadeChange + else return NoChange else do liftIO $ hPutStr stderr transcript return FailedChange @@ -78,31 +86,8 @@ letsEncrypt' (AgreeTOS memail) domain domains webroot certinstaller = 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 +-- | The cerificate files that letsencrypt will make available for a domain. liveCertDir :: Domain -> FilePath liveCertDir d = "/etc/letsencrypt/live" d -- cgit v1.2.3