1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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"
|