summaryrefslogtreecommitdiff
path: root/src/Propellor/Property/LetsEncrypt.hs
blob: 592a1e1dad5b226c40d5c91b78acc2dc4b407146 (plain)
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
-- | This module gets LetsEncrypt <https://letsencrypt.org/> certificates 
-- using CertBot <https://certbot.eff.org/>

module Propellor.Property.LetsEncrypt where

import Propellor.Base
import qualified Propellor.Property.Apt as Apt

import System.Posix.Files

-- Not using the certbot name yet, until it reaches jessie-backports and
-- testing.
installed :: Property DebianLike
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; 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).
--
-- 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 more complete
-- integration of apache with letsencrypt, that's built on top of this.
letsEncrypt :: AgreeTOS -> Domain -> WebRoot -> Property DebianLike
letsEncrypt tos domain = letsEncrypt' tos domain []

-- | Like `letsEncrypt`, but the certificate can be obtained for multiple
-- domains.
letsEncrypt' :: AgreeTOS -> Domain -> [Domain] -> WebRoot -> Property DebianLike
letsEncrypt' (AgreeTOS memail) domain domains webroot =
	prop `requires` installed
  where
	prop :: Property UnixLike
	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 MadeChange
					else return NoChange
			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"
		, "--noninteractive"
		, "--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)

-- | The cerificate files that letsencrypt will make available for a domain.
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"