summaryrefslogtreecommitdiff
path: root/src/Propellor/Property/LetsEncrypt.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Propellor/Property/LetsEncrypt.hs')
-rw-r--r--src/Propellor/Property/LetsEncrypt.hs115
1 files changed, 115 insertions, 0 deletions
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"