acmesmith 0.6.1 → 0.7.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e7fcd16986fecaf6cb855a668f261f191236f041
4
- data.tar.gz: c2995040c1e0681c9322243132e3393399fbbd07
3
+ metadata.gz: c4a9da6141839cd8a3b4850091f05ab7e024a252
4
+ data.tar.gz: 64807174e4ee28afd2a4be93d7ae8cbcdcf8cde1
5
5
  SHA512:
6
- metadata.gz: 1d2ba38ef47edf8b10a960dff3a608221dcc879a279a7713a9fa85f95141f03e67ead6aa227809951fd57b6f44fa04b6567a69c33cad53eef0f83a2f066aae51
7
- data.tar.gz: 35aefc2f03da1d5fa3c16858c9000349a2434c7f65cfea6ccffea74fdd36dc5ca6ead77d9f8acea07a94c6167b39889e92ea3fcc65b6eefe2d809d936b04cad7
6
+ metadata.gz: 1be84d5808225cf30cd6dc324a4a7924f1dacfdb310cc5872e7bdfa74d12ef93e7de5c234ba9ad4fe0679ce03ded44e832a66065544cb9dd808360fb7da4e5a3
7
+ data.tar.gz: 549065d60f3774ab1ef4be20da4b90d4f02f0e7b13af912b62128c65026ae5c1fb76880754251a3b69a929fe2d3a1581c8d09f5a8184829c57c33e317d875d4c
@@ -0,0 +1,235 @@
1
+ require 'acmesmith/account_key'
2
+ require 'acmesmith/certificate'
3
+
4
+ require 'acme-client'
5
+
6
+ module Acmesmith
7
+ class Client
8
+ def initialize(config: nil)
9
+ @config ||= config
10
+ end
11
+
12
+ def register(contact)
13
+ key = AccountKey.generate
14
+ acme = Acme::Client.new(private_key: key.private_key, endpoint: config['endpoint'])
15
+ registration = acme.register(contact: contact)
16
+ registration.agree_terms
17
+
18
+ storage.put_account_key(key, account_key_passphrase)
19
+
20
+ key
21
+ end
22
+
23
+ def authorize(*domains)
24
+ targets = domains.map do |domain|
25
+ authz = acme.authorize(domain: domain)
26
+ challenges = [authz.http01, authz.dns01, authz.tls_sni01].compact
27
+ challenge = nil
28
+ responder = config.challenge_responders.find do |x|
29
+ challenge = challenges.find { |_| x.support?(_.class::CHALLENGE_TYPE) }
30
+ end
31
+ {domain: domain, authz: authz, responder: responder, challenge: challenge}
32
+ end
33
+
34
+ begin
35
+ targets.each do |target|
36
+ target[:responder].respond(target[:domain], target[:challenge])
37
+ end
38
+
39
+ targets.each do |target|
40
+ puts "=> Requesting verifications..."
41
+ target[:challenge].request_verification
42
+ end
43
+ loop do
44
+ all_valid = true
45
+ targets.each do |target|
46
+ next if target[:valid]
47
+
48
+ status = target[:challenge].verify_status
49
+ puts " * [#{target[:domain]}] verify_status: #{status}"
50
+
51
+ if status == 'valid'
52
+ target[:valid] = true
53
+ next
54
+ end
55
+
56
+ all_valid = false
57
+ if status == "invalid"
58
+ err = target[:challenge].error
59
+ puts " ! [#{target[:domain]}] #{err["type"]}: #{err["detail"]}"
60
+ end
61
+ end
62
+ break if all_valid
63
+ sleep 3
64
+ end
65
+ puts "=> Done"
66
+ ensure
67
+ targets.each do |target|
68
+ target[:responder].cleanup(target[:domain], target[:challenge])
69
+ end
70
+ end
71
+ end
72
+
73
+ def request(common_name, *sans)
74
+ csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: sans)
75
+ retried = false
76
+ acme_cert = begin
77
+ acme.new_certificate(csr)
78
+ rescue Acme::Client::Error::Unauthorized => e
79
+ raise unless config.auto_authorize_on_request
80
+ raise if retried
81
+
82
+ puts "=> Authorizing unauthorized domain names"
83
+ # https://github.com/letsencrypt/boulder/blob/b9369a481415b3fe31e010b34e2ff570b89e42aa/ra/ra.go#L604
84
+ m = e.message.match(/authorizations for these names not found or expired: ((?:[a-zA-Z0-9_.\-]+(?:,\s+|$))+)/)
85
+ if m && m[1]
86
+ domains = m[1].split(/,\s+/)
87
+ else
88
+ warn " ! Error message on certificate request was #{e.message.inspect} and acmesmith couldn't determine which domain names are unauthorized (maybe a bug)"
89
+ warn " ! Attempting to authorize all domains in this certificate reuqest for now."
90
+ domains = [common_name, *sans]
91
+ end
92
+ puts " * #{domains.join(', ')}"
93
+ authorize(*domains)
94
+ retried = true
95
+ retry
96
+ end
97
+
98
+ cert = Certificate.from_acme_client_certificate(acme_cert)
99
+ storage.put_certificate(cert, certificate_key_passphrase)
100
+
101
+ execute_post_issue_hooks(common_name)
102
+
103
+ cert
104
+ end
105
+
106
+ def execute_post_issue_hooks(common_name)
107
+ post_issues_hooks_for_common_name = config.post_issueing_hooks(common_name)
108
+ if post_issues_hooks_for_common_name
109
+ post_issues_hooks_for_common_name.each do |hook|
110
+ hook.execute
111
+ end
112
+ end
113
+ end
114
+
115
+ def certificate_versions(common_name)
116
+ storage.list_certificate_versions(common_name).sort
117
+ end
118
+
119
+ def certificates_list
120
+ storage.list_certificates.sort
121
+ end
122
+
123
+ def current(common_name)
124
+ storage.get_current_certificate_version(common_name)
125
+ end
126
+
127
+ def get_certificate(common_name, version: 'current', type: 'text')
128
+ cert = storage.get_certificate(common_name, version: version)
129
+
130
+ certs = []
131
+ case type
132
+ when 'text'
133
+ certs << cert.certificate.to_text
134
+ certs << cert.certificate.to_pem
135
+ when 'certificate'
136
+ certs << cert.certificate.to_pem
137
+ when 'chain'
138
+ certs << cert.chain
139
+ when 'fullchain'
140
+ certs << cert.fullchain
141
+ end
142
+
143
+ certs
144
+ end
145
+
146
+ def save_certificate(common_name, version: 'current', mode: '0600', output:)
147
+ cert = storage.get_certificate(common_name, version: version)
148
+ File.open(output, 'w', mode.to_i(8)) do |f|
149
+ f.puts(cert.fullchain)
150
+ end
151
+ end
152
+
153
+ def get_private_key(common_name, version: 'current')
154
+ cert = storage.get_certificate(common_name, version: version)
155
+ cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
156
+
157
+ cert.private_key.to_pem
158
+ end
159
+
160
+ def save_private_key(common_name, version: 'current', mode: '0600', output:)
161
+ cert = storage.get_certificate(common_name, version: version)
162
+ cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
163
+ File.open(output, 'w', mode.to_i(8)) do |f|
164
+ f.puts(cert.private_key)
165
+ end
166
+ end
167
+
168
+ def save_pkcs12(common_name, version: 'current', mode: '0600', output:, passphrase:)
169
+ cert = storage.get_certificate(common_name, version: version)
170
+ cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
171
+
172
+ p12 = OpenSSL::PKCS12.create(passphrase, cert.common_name, cert.private_key, cert.certificate)
173
+ File.open(output, 'w', mode.to_i(8)) do |f|
174
+ f.puts p12.to_der
175
+ end
176
+ end
177
+
178
+ def autorenew(days)
179
+ storage.list_certificates.each do |cn|
180
+ puts "=> #{cn}"
181
+ cert = storage.get_certificate(cn)
182
+ not_after = cert.certificate.not_after.utc
183
+
184
+ puts " Not valid after: #{not_after}"
185
+ next unless (cert.certificate.not_after.utc - Time.now.utc) < (days.to_i * 86400)
186
+ puts " * Renewing: CN=#{cert.common_name}, SANs=#{cert.sans.join(',')}"
187
+ request(cert.common_name, *cert.sans)
188
+ end
189
+ end
190
+
191
+ def add_san(common_name, *add_sans)
192
+ puts "=> reissuing CN=#{common_name} with new SANs #{add_sans.join(?,)}"
193
+ cert = storage.get_certificate(common_name)
194
+ sans = cert.sans + add_sans
195
+ puts " * SANs will be: #{sans.join(?,)}"
196
+ request(cert.common_name, *sans)
197
+ end
198
+
199
+ private
200
+
201
+ def config
202
+ @config
203
+ end
204
+
205
+ def storage
206
+ config.storage
207
+ end
208
+
209
+ def account_key
210
+ @account_key ||= storage.get_account_key.tap do |x|
211
+ x.key_passphrase = account_key_passphrase if account_key_passphrase
212
+ end
213
+ end
214
+
215
+ def acme
216
+ @acme ||= Acme::Client.new(private_key: account_key.private_key, endpoint: config['endpoint'])
217
+ end
218
+
219
+ def certificate_key_passphrase
220
+ if config['passphrase_from_env']
221
+ ENV['ACMESMITH_CERTIFICATE_KEY_PASSPHRASE'] || config['certificate_key_passphrase']
222
+ else
223
+ config['certificate_key_passphrase']
224
+ end
225
+ end
226
+
227
+ def account_key_passphrase
228
+ if config['passphrase_from_env']
229
+ ENV['ACMESMITH_ACCOUNT_KEY_PASSPHRASE'] || config['account_key_passphrase']
230
+ else
231
+ config['account_key_passphrase']
232
+ end
233
+ end
234
+ end
235
+ end
@@ -1,10 +1,7 @@
1
1
  require 'thor'
2
2
 
3
3
  require 'acmesmith/config'
4
- require 'acmesmith/account_key'
5
- require 'acmesmith/certificate'
6
-
7
- require 'acme-client'
4
+ require 'acmesmith/client'
8
5
 
9
6
  module Acmesmith
10
7
  class Command < Thor
@@ -13,138 +10,48 @@ module Acmesmith
13
10
 
14
11
  desc "register CONTACT", "Create account key (contact e.g. mailto:xxx@example.org)"
15
12
  def register(contact)
16
- key = AccountKey.generate
17
- acme = Acme::Client.new(private_key: key.private_key, endpoint: config['endpoint'])
18
- registration = acme.register(contact: contact)
19
- registration.agree_terms
20
-
21
- storage.put_account_key(key, account_key_passphrase)
13
+ key = client.register(contact)
22
14
  puts "Generated:\n#{key.private_key.public_key.to_pem}"
23
15
  end
24
16
 
25
17
  desc "authorize DOMAIN [DOMAIN ...]", "Get authz for DOMAIN."
26
18
  def authorize(*domains)
27
- targets = domains.map do |domain|
28
- authz = acme.authorize(domain: domain)
29
- challenges = [authz.http01, authz.dns01, authz.tls_sni01].compact
30
- challenge = nil
31
- responder = config.challenge_responders.find do |x|
32
- challenge = challenges.find { |_| x.support?(_.class::CHALLENGE_TYPE) }
33
- end
34
- {domain: domain, authz: authz, responder: responder, challenge: challenge}
35
- end
36
-
37
- begin
38
- targets.each do |target|
39
- target[:responder].respond(target[:domain], target[:challenge])
40
- end
41
-
42
- targets.each do |target|
43
- puts "=> Requesting verifications..."
44
- target[:challenge].request_verification
45
- end
46
- loop do
47
- all_valid = true
48
- targets.each do |target|
49
- next if target[:valid]
50
-
51
- status = target[:challenge].verify_status
52
- puts " * [#{target[:domain]}] verify_status: #{status}"
53
-
54
- if status == 'valid'
55
- target[:valid] = true
56
- next
57
- end
58
-
59
- all_valid = false
60
- if status == "invalid"
61
- err = target[:challenge].error
62
- puts " ! [#{target[:domain]}] #{err["type"]}: #{err["detail"]}"
63
- end
64
- end
65
- break if all_valid
66
- sleep 3
67
- end
68
- puts "=> Done"
69
- ensure
70
- targets.each do |target|
71
- target[:responder].cleanup(target[:domain], target[:challenge])
72
- end
73
- end
19
+ client.authorize(*domains)
74
20
  end
75
21
 
76
22
  desc "request COMMON_NAME [SAN]", "request certificate for CN +COMMON_NAME+ with SANs +SAN+"
77
23
  def request(common_name, *sans)
78
- csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: sans)
79
- retried = false
80
- acme_cert = begin
81
- acme.new_certificate(csr)
82
- rescue Acme::Client::Error::Unauthorized => e
83
- raise unless config.auto_authorize_on_request
84
- raise if retried
85
-
86
- puts "=> Authorizing unauthorized domain names"
87
- # https://github.com/letsencrypt/boulder/blob/b9369a481415b3fe31e010b34e2ff570b89e42aa/ra/ra.go#L604
88
- m = e.message.match(/authorizations for these names not found or expired: ((?:[a-zA-Z0-9_.\-]+(?:,\s+|$))+)/)
89
- if m && m[1]
90
- domains = m[1].split(/,\s+/)
91
- else
92
- warn " ! Error message on certificate request was #{e.message.inspect} and acmesmith couldn't determine which domain names are unauthorized (maybe a bug)"
93
- warn " ! Attempting to authorize all domains in this certificate reuqest for now."
94
- domains = [common_name, *sans]
95
- end
96
- puts " * #{domains.join(', ')}"
97
- authorize(*domains)
98
- retried = true
99
- retry
100
- end
101
-
102
- cert = Certificate.from_acme_client_certificate(acme_cert)
103
- storage.put_certificate(cert, certificate_key_passphrase)
104
-
24
+ cert = client.request(common_name, *sans)
105
25
  puts cert.certificate.to_text
106
26
  puts cert.certificate.to_pem
107
-
108
- execute_post_issue_hooks(common_name)
109
27
  end
110
28
 
111
29
  desc "post-issue-hooks COMMON_NAME", "Run all post-issueing hooks for common name. (for testing purpose)"
112
30
  def post_issue_hooks(common_name)
113
- execute_post_issue_hooks(common_name)
31
+ client.post_issue_hooks(common_name)
114
32
  end
115
33
  map 'post-issue-hooks' => :post_issue_hooks
116
34
 
117
35
  desc "list [COMMON_NAME]", "list certificates or its versions"
118
36
  def list(common_name = nil)
119
37
  if common_name
120
- puts storage.list_certificate_versions(common_name).sort
38
+ puts client.certificate_versions(common_name)
121
39
  else
122
- puts storage.list_certificates.sort
40
+ puts client.certificates_list
123
41
  end
124
42
  end
125
43
 
126
44
  desc "current COMMON_NAME", "show current version for certificate"
127
45
  def current(common_name)
128
- puts storage.get_current_certificate_version(common_name)
46
+ puts client.current(common_name)
129
47
  end
130
48
 
131
49
  desc "show-certificate COMMON_NAME", "show certificate"
132
50
  method_option :version, type: :string, default: 'current'
133
51
  method_option :type, type: :string, enum: %w(text certificate chain fullchain), default: 'text'
134
52
  def show_certificate(common_name)
135
- cert = storage.get_certificate(common_name, version: options[:version])
136
-
137
- case options[:type]
138
- when 'text'
139
- puts cert.certificate.to_text
140
- puts cert.certificate.to_pem
141
- when 'certificate'
142
- puts cert.certificate.to_pem
143
- when 'chain'
144
- puts cert.chain
145
- when 'fullchain'
146
- puts cert.fullchain
147
- end
53
+ certs = client.get_certificate(common_name, version: options[:version], type: options[:type])
54
+ puts certs
148
55
  end
149
56
  map 'show-certiticate' => :show_certificate
150
57
 
@@ -153,19 +60,13 @@ module Acmesmith
153
60
  method_option :output, type: :string, required: true, banner: 'PATH', desc: 'Path to output file'
154
61
  method_option :mode, type: :string, default: '0600', desc: 'Mode (permission) of the output file on create'
155
62
  def save_certificate(common_name)
156
- cert = storage.get_certificate(common_name, version: options[:version])
157
- File.open(options[:output], 'w', options[:mode].to_i(8)) do |f|
158
- f.puts(cert.fullchain)
159
- end
63
+ client.save_certificate(common_name, version: options[:version], mode: options[:mode], output: options[:output])
160
64
  end
161
65
 
162
66
  desc "show-private-key COMMON_NAME", "show private key"
163
67
  method_option :version, type: :string, default: 'current'
164
68
  def show_private_key(common_name)
165
- cert = storage.get_certificate(common_name, version: options[:version])
166
- cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
167
-
168
- puts cert.private_key.to_pem
69
+ puts client.get_private_key(common_name, version: options[:version])
169
70
  end
170
71
  map 'show-private-key' => :show_private_key
171
72
 
@@ -174,11 +75,7 @@ module Acmesmith
174
75
  method_option :output, type: :string, required: true, banner: 'PATH', desc: 'Path to output file'
175
76
  method_option :mode, type: :string, default: '0600', desc: 'Mode (permission) of the output file on create'
176
77
  def save_private_key(common_name)
177
- cert = storage.get_certificate(common_name, version: options[:version])
178
- cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
179
- File.open(options[:output], 'w', options[:mode].to_i(8)) do |f|
180
- f.puts(cert.private_key)
181
- end
78
+ client.save_private_key(common_name, version: options[:version], mode: options[:mode], output: options[:output])
182
79
  end
183
80
 
184
81
  desc 'save-pkcs12 COMMON_NAME', 'Save ceriticate and private key to .p12 file'
@@ -186,9 +83,6 @@ module Acmesmith
186
83
  method_option :output, type: :string, required: true, banner: 'PATH', desc: 'Path to output file'
187
84
  method_option :mode, type: :string, default: '0600', desc: 'Mode (permission) of the output file on create'
188
85
  def save_pkcs12(common_name)
189
- cert = storage.get_certificate(common_name, version: options[:version])
190
- cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
191
-
192
86
  print 'Passphrase: '
193
87
  passphrase = $stdin.noecho { $stdin.gets }.chomp
194
88
  print "\nPassphrase (confirm): "
@@ -196,81 +90,26 @@ module Acmesmith
196
90
  puts
197
91
 
198
92
  raise ArgumentError, "Passphrase doesn't match" if passphrase != passphrase2
199
-
200
- p12 = OpenSSL::PKCS12.create(passphrase, cert.common_name, cert.private_key, cert.certificate)
201
- File.open(options[:output], 'w', options[:mode].to_i(8)) do |f|
202
- f.puts p12.to_der
203
- end
93
+ client.save_pkcs12(common_name, version: options[:version], mode: options[:mode], output: options[:output], passphrase: passphrase)
204
94
  end
205
95
 
206
96
  desc "autorenew", "request renewal of certificates which expires soon"
207
97
  method_option :days, type: :numeric, aliases: %w(-d), default: 7, desc: 'specify threshold in days to select certificates to renew'
208
98
  def autorenew
209
- storage.list_certificates.each do |cn|
210
- puts "=> #{cn}"
211
- cert = storage.get_certificate(cn)
212
- not_after = cert.certificate.not_after.utc
213
-
214
- puts " Not valid after: #{not_after}"
215
- next unless (cert.certificate.not_after.utc - Time.now.utc) < (options[:days].to_i * 86400)
216
- puts " * Renewing: CN=#{cert.common_name}, SANs=#{cert.sans.join(',')}"
217
- request(cert.common_name, *cert.sans)
218
- end
99
+ client.autorenew(options[:days])
219
100
  end
220
101
 
221
102
  desc "add-san COMMON_NAME [ADDITIONAL_SANS]", "request renewal of existing certificate with additional SANs"
222
103
  def add_san(common_name, *add_sans)
223
- puts "=> reissuing CN=#{common_name} with new SANs #{add_sans.join(?,)}"
224
- cert = storage.get_certificate(common_name)
225
- sans = cert.sans + add_sans
226
- puts " * SANs will be: #{sans.join(?,)}"
227
- request(cert.common_name, *sans)
104
+ client.add_san(common_name, *add_sans)
228
105
  end
229
106
 
230
107
  private
231
108
 
232
- def config
233
- @config ||= Config.load_yaml(options[:config])
234
- end
235
-
236
- def storage
237
- config.storage
109
+ def client
110
+ config = Config.load_yaml(options[:config])
111
+ config.merge!("passphrase_from_env" => options[:passphrase_from_env]) unless options[:passphrase_from_env].nil?
112
+ @client = Client.new(config: config)
238
113
  end
239
-
240
- def account_key
241
- @account_key ||= storage.get_account_key.tap do |x|
242
- x.key_passphrase = account_key_passphrase if account_key_passphrase
243
- end
244
- end
245
-
246
- def acme
247
- @acme ||= Acme::Client.new(private_key: account_key.private_key, endpoint: config['endpoint'])
248
- end
249
-
250
- def certificate_key_passphrase
251
- if options[:passphrase_from_env] || config['passphrase_from_env']
252
- ENV['ACMESMITH_CERTIFICATE_KEY_PASSPHRASE'] || config['certificate_key_passphrase']
253
- else
254
- config['certificate_key_passphrase']
255
- end
256
- end
257
-
258
- def account_key_passphrase
259
- if options[:passphrase_from_env] || config['passphrase_from_env']
260
- ENV['ACMESMITH_ACCOUNT_KEY_PASSPHRASE'] || config['account_key_passphrase']
261
- else
262
- config['account_key_passphrase']
263
- end
264
- end
265
-
266
- def execute_post_issue_hooks(common_name)
267
- post_issues_hooks_for_common_name = config.post_issueing_hooks(common_name)
268
- if post_issues_hooks_for_common_name
269
- post_issues_hooks_for_common_name.each do |hook|
270
- hook.execute
271
- end
272
- end
273
- end
274
-
275
114
  end
276
115
  end
@@ -28,6 +28,10 @@ module Acmesmith
28
28
  @config[key]
29
29
  end
30
30
 
31
+ def merge!(pair)
32
+ @config.merge!(pair)
33
+ end
34
+
31
35
  def account_key_passphrase
32
36
  @config['account_key_passphrase']
33
37
  end
@@ -1,3 +1,3 @@
1
1
  module Acmesmith
2
- VERSION = "0.6.1"
2
+ VERSION = "0.7.0.beta1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acmesmith
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - sorah (Shota Fukumori)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-06 00:00:00.000000000 Z
11
+ date: 2017-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acme-client
@@ -124,6 +124,7 @@ files:
124
124
  - lib/acmesmith/challenge_responders.rb
125
125
  - lib/acmesmith/challenge_responders/base.rb
126
126
  - lib/acmesmith/challenge_responders/route53.rb
127
+ - lib/acmesmith/client.rb
127
128
  - lib/acmesmith/command.rb
128
129
  - lib/acmesmith/config.rb
129
130
  - lib/acmesmith/post_issueing_hooks.rb
@@ -152,12 +153,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
153
  version: '0'
153
154
  required_rubygems_version: !ruby/object:Gem::Requirement
154
155
  requirements:
155
- - - ">="
156
+ - - ">"
156
157
  - !ruby/object:Gem::Version
157
- version: '0'
158
+ version: 1.3.1
158
159
  requirements: []
159
160
  rubyforge_project:
160
- rubygems_version: 2.6.8
161
+ rubygems_version: 2.6.11
161
162
  signing_key:
162
163
  specification_version: 4
163
164
  summary: ACME client (Let's encrypt client) to manage certificate in multi server