blix-letsencrypt 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +98 -0
- data/bin/letsencrypt +3 -0
- data/lib/blix/letsencrypt.rb +319 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 51750c9fbd2104f084d61f0a07d3bc0be338cacdb935850b35cf3f427659f1c9
|
4
|
+
data.tar.gz: 0ec555ca56aa54faad52f135cb0b5e1bbb87a9c2420d783001a2bec9d64913b0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 47f6145bfc9b098bfefd2dae95c0124a257060a473ec6922b2e5a9f8577b5623ab4c96ace1b3910abe59b59354c6e96f56968e09483b195deeae476d8988b106
|
7
|
+
data.tar.gz: 75e034dc207dba70fe331275c071a66c067ac07bf0b005a5f10c285aa4b272ba74af501d591e498e26d9d63dccc204d35b7c407ea4bbfbf4db42bfac0e783a52
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Clive Andrews
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# letsencrypt
|
2
|
+
|
3
|
+
a command line utility for managing letsencrypt ssl certificates.
|
4
|
+
|
5
|
+
|
6
|
+
## depends
|
7
|
+
|
8
|
+
ruby >= 2.4
|
9
|
+
|
10
|
+
## install
|
11
|
+
|
12
|
+
gem install blix-letsencrypt
|
13
|
+
|
14
|
+
## command options:
|
15
|
+
|
16
|
+
Usage: letsencrypt [options]
|
17
|
+
-c, --create Create ACME private key
|
18
|
+
-k, --key=FILE ACME private key file
|
19
|
+
-e, --email=EMAIL your contact email
|
20
|
+
-d, --domain=DOMAIN domain name for certificate
|
21
|
+
--challenge_dir=CDIR challenge file directory
|
22
|
+
--ssl_dir=SSLDIR ssl certificate file directory
|
23
|
+
--ssl_key=SSLKEY ssl private key file
|
24
|
+
-t, --test enable test mode
|
25
|
+
--force force update even if not expired
|
26
|
+
-l, --logfile=LOGFILE log to file
|
27
|
+
-h, --hook=HOOK script to run on renewal
|
28
|
+
|
29
|
+
|
30
|
+
## conventions used
|
31
|
+
|
32
|
+
* the private key is called `privkey.pem`
|
33
|
+
|
34
|
+
|
35
|
+
* the certificate is called `cert.pem` and is placed in a directory named
|
36
|
+
after the main (first) domain name.
|
37
|
+
|
38
|
+
## create letsencrypt certificates
|
39
|
+
|
40
|
+
* create directory to hold your keys and certificates .. eg:
|
41
|
+
|
42
|
+
mkdir /etc/letsencrypt/account
|
43
|
+
mkdir /etc/letsencrypt/ssl
|
44
|
+
|
45
|
+
* create directory to serve challenges from.. eg:
|
46
|
+
|
47
|
+
mkdir /srv/certbot/.well-known
|
48
|
+
|
49
|
+
* create a ssl private key if you do not yet have one.. eg:
|
50
|
+
|
51
|
+
openssl genrsa -out /etc/letsencrypt/ssl/privkey.pem 2048
|
52
|
+
|
53
|
+
* update your webserver to serve the challengefiles eg for nginx..:
|
54
|
+
|
55
|
+
location /.well-known {
|
56
|
+
alias /srv/certbot/.well-known;
|
57
|
+
add_header "Content-Type" "text/plain";
|
58
|
+
break;
|
59
|
+
}
|
60
|
+
|
61
|
+
* now create your certificate
|
62
|
+
|
63
|
+
letsencrypt --key=/etc/letsencrypt/account/key.pem -d"example.com www.example.com" --challenge_dir="/srv/certbot/.well-known" --ssl_dir="/etc/letsencrypt/ssl" --logfile=/var/log/letsencrypt.log --create
|
64
|
+
|
65
|
+
* hopefully your certificate has be created so update your webserver to use it...
|
66
|
+
|
67
|
+
ssl_certificate /etc/letsencrypt/ssl/example.com/cert.pem;
|
68
|
+
ssl_certificate_key /etc/letsencrypt/ssl/privkey.pem;
|
69
|
+
|
70
|
+
* reload the webserver and check all is well.
|
71
|
+
|
72
|
+
## auto renew letsencrypt certificates
|
73
|
+
|
74
|
+
the letsencrypt certificates are valid for 90 days. it is recommended that you
|
75
|
+
run a script every day to check if the certificates are due for renewal.
|
76
|
+
|
77
|
+
* create two shell scrips, one to renew the certificates and another to
|
78
|
+
restart the webserver.
|
79
|
+
|
80
|
+
* ensure that both scripts are executable..
|
81
|
+
* copy the first script to /etc/cron.daily directory.
|
82
|
+
* link the second script to the `--hook` option of the letsencrypt command.
|
83
|
+
|
84
|
+
eg:
|
85
|
+
|
86
|
+
cat /etc/cron.daily/renew_ssl
|
87
|
+
|
88
|
+
!/bin/sh
|
89
|
+
/opt/ruby-2.6.4/bin/letsencrypt --key=/etc/letsencrypt/account/key.pem \
|
90
|
+
-d"example.com www.example.com" \
|
91
|
+
--challenge_dir="/srv/certbot/.well-known" --ssl_dir="/etc/letsencrypt/ssl" \
|
92
|
+
--logfile=/var/log/letsencrypt.log \
|
93
|
+
--hook=/root/bin/reload_nginx
|
94
|
+
|
95
|
+
cat /root/bin/reload_nginx
|
96
|
+
|
97
|
+
!/bin/sh
|
98
|
+
/sbin/nginx -t && /sbin/nginx -sreload
|
data/bin/letsencrypt
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# this is a script for renewing letsencrypt certicates. .. to automate the
|
4
|
+
# process create a shell script to run this command and a shell script to
|
5
|
+
# restart the web server
|
6
|
+
#
|
7
|
+
# ensure that both scripts are executable..
|
8
|
+
# copy the first script to /etc/cron.daily directory.
|
9
|
+
# link the second script to the --hook option of this ruby command.
|
10
|
+
#
|
11
|
+
# eg:
|
12
|
+
#
|
13
|
+
# cat /etc/cron.daily/renew_ssl
|
14
|
+
#
|
15
|
+
# #!/bin/sh
|
16
|
+
# /opt/ruby-2.6.4/bin/ruby /root/bin/letsencrypt.rb --key=/etc/letsencrypt/account/key.pem \
|
17
|
+
# -d"example.com www.example.com" \
|
18
|
+
# --challenge_dir="/tmp/certbot/public_html/.well-known" \
|
19
|
+
# --ssl_dir="/etc/letsencrypt/ssl" \
|
20
|
+
# --logfile=/var/log/letsencrypt.log \
|
21
|
+
# --hook=/root/bin/reload_nginx
|
22
|
+
#
|
23
|
+
#
|
24
|
+
# cat /root/bin/reload_nginx
|
25
|
+
# #!/bin/sh
|
26
|
+
# /opt/nginx-1.2.2/sbin/nginx -t && /opt/nginx-1.2.2/sbin/nginx -sreload
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# copyright Clive Andrews 2020
|
30
|
+
#
|
31
|
+
# licence MIT
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
require 'openssl'
|
36
|
+
require 'optparse'
|
37
|
+
require 'acme-client'
|
38
|
+
require 'logger'
|
39
|
+
|
40
|
+
# location /.well-known{
|
41
|
+
# rewrite ^\.well-known(.*)$ $1 break;
|
42
|
+
# alias /srv/letsencrypt/challenge;
|
43
|
+
# add_header Content-Type text/plain;
|
44
|
+
# }
|
45
|
+
|
46
|
+
CHALLENGE_DIR = '/srv/letsencrypt'
|
47
|
+
SSL_DIR = '/etc/letsencrypt/cert'
|
48
|
+
SSL_CERT = 'cert.pem'
|
49
|
+
SSL_KEY = 'privkey.pem'
|
50
|
+
ACME_DIR_S = 'https://acme-staging-v02.api.letsencrypt.org/directory' # letsencrypt
|
51
|
+
ACME_DIR_L = 'https://acme-v02.api.letsencrypt.org/directory' # letsencrypt live
|
52
|
+
|
53
|
+
TIMEOUT = 60
|
54
|
+
|
55
|
+
# check if certificate is about to expire.
|
56
|
+
def certificate_expiry_is_soon(ssl_dir, certificate_file, days = 30)
|
57
|
+
file = File.join(ssl_dir, certificate_file)
|
58
|
+
return true unless File.file?(file)
|
59
|
+
|
60
|
+
cert = OpenSSL::X509::Certificate.new(File.read(file))
|
61
|
+
cert.not_after < Time.now + days * (24 * 60 * 60)
|
62
|
+
end
|
63
|
+
|
64
|
+
def tidy_challenge_file(file)
|
65
|
+
file = file[1..-1] if file[0, 1] == '/'
|
66
|
+
str = '.well-known'
|
67
|
+
file = file[str.length..-1] if file[0, str.length] == str
|
68
|
+
file
|
69
|
+
end
|
70
|
+
|
71
|
+
# write the challenge file and ensure that intermediate dirs exist
|
72
|
+
def write_file(dir, file, content)
|
73
|
+
file = tidy_challenge_file(file)
|
74
|
+
parts = file.split('/')
|
75
|
+
last_index = parts.length - 1
|
76
|
+
path = nil
|
77
|
+
parts.each_with_index do |_part, idx|
|
78
|
+
path = File.join(dir, *parts[0, idx + 1])
|
79
|
+
if idx == last_index # the file name
|
80
|
+
File.write(path, content)
|
81
|
+
else
|
82
|
+
if File.file?(path)
|
83
|
+
raise "invalid challenge path: #{path}"
|
84
|
+
elsif File.directory?(path)
|
85
|
+
|
86
|
+
else
|
87
|
+
Dir.mkdir(path)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
path
|
92
|
+
end
|
93
|
+
|
94
|
+
def backup_file(dir, file)
|
95
|
+
orig_path = File.join(dir, file)
|
96
|
+
orig_file = File.basename(orig_path)
|
97
|
+
orig_dir = File.dirname(orig_path)
|
98
|
+
|
99
|
+
raise "backup file does not exist:#{orig_path}" unless File.exist?(orig_path)
|
100
|
+
|
101
|
+
seq = 1
|
102
|
+
loop do
|
103
|
+
prefix = '%04d_' % seq
|
104
|
+
new_file = prefix + orig_file
|
105
|
+
new_path = File.join(orig_dir, new_file)
|
106
|
+
if File.exist?(new_path)
|
107
|
+
seq += 1
|
108
|
+
next
|
109
|
+
else
|
110
|
+
content = File.read(orig_path)
|
111
|
+
File.write(new_path, content)
|
112
|
+
break new_path
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# delete the challenge files
|
118
|
+
def remove_file(dir, file)
|
119
|
+
file = tidy_challenge_file(file)
|
120
|
+
path = File.join(dir, file)
|
121
|
+
File.unlink(path) if File.file?(path)
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
# perform an authorization by creating the challenge files
|
126
|
+
# and waiting for validation to occur.
|
127
|
+
def perform_authorization(challenge_dir, authorization)
|
128
|
+
http_challenge = authorization.http
|
129
|
+
|
130
|
+
# write the challenge to file
|
131
|
+
|
132
|
+
http_challenge.content_type # => 'text/plain'
|
133
|
+
http_challenge.file_content # => example_token.TO1xJ0UDgfQ8WY5zT3txynup87UU3PhcDEIcuPyw4QU
|
134
|
+
http_challenge.filename # => '.well-known/acme-challenge/example_token'
|
135
|
+
http_challenge.token
|
136
|
+
|
137
|
+
challenge_file = tidy_challenge_file(http_challenge.filename)
|
138
|
+
challenge_path = write_file(challenge_dir, challenge_file, http_challenge.file_content)
|
139
|
+
|
140
|
+
puts "challenge has been written to :#{challenge_path}"
|
141
|
+
|
142
|
+
# now wait for the challenge ..
|
143
|
+
|
144
|
+
http_challenge.request_validation
|
145
|
+
timeout_time = Time.now + TIMEOUT
|
146
|
+
|
147
|
+
while http_challenge.status == 'pending'
|
148
|
+
if Time.now > timeout_time
|
149
|
+
remove_file(challenge_dir, challenge_file)
|
150
|
+
raise 'Challenge timeout'
|
151
|
+
end
|
152
|
+
sleep(2)
|
153
|
+
http_challenge.reload
|
154
|
+
end
|
155
|
+
|
156
|
+
remove_file(challenge_dir, challenge_file)
|
157
|
+
raise 'challenge failed' unless http_challenge.status == 'valid' # => 'valid'
|
158
|
+
end
|
159
|
+
|
160
|
+
# handle options here
|
161
|
+
options = {}
|
162
|
+
OptionParser.new do |opts|
|
163
|
+
# opts.banner = "Usage: example.rb [options]"
|
164
|
+
|
165
|
+
opts.on('-c', '--create', 'Create ACME private key') do |_v|
|
166
|
+
options[:create] = true
|
167
|
+
end
|
168
|
+
|
169
|
+
opts.on('-k', '--key=FILE', 'ACME private key file') do |v|
|
170
|
+
options[:key] = v
|
171
|
+
end
|
172
|
+
|
173
|
+
opts.on('-e', '--email=EMAIL', 'your contact email') do |v|
|
174
|
+
options[:email] = v
|
175
|
+
end
|
176
|
+
|
177
|
+
opts.on('-d', '--domain=DOMAIN', 'domain name for certificate') do |v|
|
178
|
+
options[:site] = v
|
179
|
+
end
|
180
|
+
|
181
|
+
opts.on('--challenge_dir=CDIR', 'challenge file directory') do |v|
|
182
|
+
options[:challenge_dir] = v
|
183
|
+
end
|
184
|
+
|
185
|
+
opts.on('--ssl_dir=SSLDIR', 'ssl certificate file directory') do |v|
|
186
|
+
options[:ssl_dir] = v
|
187
|
+
end
|
188
|
+
|
189
|
+
opts.on('--ssl_key=SSLKEY', 'ssl private key file') do |v|
|
190
|
+
options[:ssl_dir] = v
|
191
|
+
end
|
192
|
+
|
193
|
+
opts.on('-t', '--test', 'enable test mode') do |v|
|
194
|
+
options[:test] = v
|
195
|
+
end
|
196
|
+
|
197
|
+
opts.on('--force', 'force update even if not expired') do |v|
|
198
|
+
options[:force] = v
|
199
|
+
end
|
200
|
+
|
201
|
+
opts.on('-l', '--logfile=LOGFILE', 'log to file') do |v|
|
202
|
+
options[:logfile] = v
|
203
|
+
end
|
204
|
+
|
205
|
+
opts.on('-h', '--hook=HOOK', 'script to run on renewal') do |v|
|
206
|
+
options[:hook] = v
|
207
|
+
end
|
208
|
+
end.parse!
|
209
|
+
|
210
|
+
# check that we have sensible values four our options before we start the
|
211
|
+
# whole [rpcess]
|
212
|
+
domains = options[:site].to_s
|
213
|
+
domains.gsub!(',', ' ')
|
214
|
+
domains.gsub!(';', ' ')
|
215
|
+
domains.gsub!(/ +/, ' ')
|
216
|
+
names = domains.split(' ')
|
217
|
+
site = names[0]
|
218
|
+
ssl_dir = File.expand_path(options[:ssl_dir] || SSL_DIR)
|
219
|
+
challenge_dir = File.expand_path(options[:challenge_dir] || CHALLENGE_DIR)
|
220
|
+
ssl_key_path = options[:ssl_key] || File.join(ssl_dir, SSL_KEY)
|
221
|
+
hook_path = options[:hook]
|
222
|
+
|
223
|
+
raise 'domain name missing' unless site
|
224
|
+
raise 'invalid challenge directory' unless File.directory?(challenge_dir)
|
225
|
+
raise 'invalid ssl certificate directory' unless File.directory?(ssl_dir)
|
226
|
+
raise "ssl private key invalid:#{ssl_key_path}" unless File.file?(ssl_key_path)
|
227
|
+
raise "script missing or not executable:#{hook_path}" unless !hook_path || File.executable?(hook_path)
|
228
|
+
|
229
|
+
certificate_file = File.join(site, SSL_CERT)
|
230
|
+
acme_key = File.expand_path(options[:key])
|
231
|
+
ssl_key = OpenSSL::PKey::RSA.new(File.read(ssl_key_path))
|
232
|
+
|
233
|
+
logger = Logger.new(options[:logfile] || STDOUT)
|
234
|
+
|
235
|
+
# check to see if the certificate is due for renewal. if the
|
236
|
+
# certificate expires within 30 days then renew otherwise exit
|
237
|
+
# unless the force option is set.
|
238
|
+
unless options[:force]
|
239
|
+
unless certificate_expiry_is_soon(ssl_dir, certificate_file)
|
240
|
+
logger.info "certificate:#{certificate_file} not due for renewal"
|
241
|
+
exit
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# first read our private key..
|
246
|
+
if File.file?(acme_key)
|
247
|
+
private_key = OpenSSL::PKey::RSA.new(File.read(acme_key)) # read
|
248
|
+
elsif options[:create]
|
249
|
+
private_key = OpenSSL::PKey::RSA.new(4096) # generate
|
250
|
+
File.write(acme_key, private_key)
|
251
|
+
else
|
252
|
+
raise "acme key file:#{acme_key} not found"
|
253
|
+
end
|
254
|
+
|
255
|
+
client = if options[:test]
|
256
|
+
Acme::Client.new(:private_key => private_key, :directory => ACME_DIR_S)
|
257
|
+
else
|
258
|
+
Acme::Client.new(:private_key => private_key, :directory => ACME_DIR_L)
|
259
|
+
end
|
260
|
+
|
261
|
+
# ensure that we hav an account.
|
262
|
+
kid = begin
|
263
|
+
client.kid
|
264
|
+
rescue StandardError
|
265
|
+
nil
|
266
|
+
end
|
267
|
+
|
268
|
+
unless kid
|
269
|
+
email = options[:email] || begin
|
270
|
+
print('enter your email:')
|
271
|
+
gets.strip
|
272
|
+
end
|
273
|
+
raise "invalid email:#{email}" unless email && email =~ /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/
|
274
|
+
|
275
|
+
account = client.new_account(:contact => "mailto:#{email}", :terms_of_service_agreed => true)
|
276
|
+
end
|
277
|
+
|
278
|
+
puts 'account found..'
|
279
|
+
|
280
|
+
# now set up our order ..
|
281
|
+
|
282
|
+
puts "setting up domain #{site}:#{names.join(',')}:"
|
283
|
+
|
284
|
+
order = client.new_order(:identifiers => names)
|
285
|
+
order.authorizations.each { |auth| perform_authorization(challenge_dir, auth) }
|
286
|
+
|
287
|
+
# download the certificate
|
288
|
+
|
289
|
+
puts 'now obtaining certificate..'
|
290
|
+
|
291
|
+
csr = Acme::Client::CertificateRequest.new(:private_key => ssl_key, :names => names, :subject => { :common_name => site })
|
292
|
+
order.finalize(:csr => csr)
|
293
|
+
|
294
|
+
timeout_time = Time.now + TIMEOUT
|
295
|
+
while order.status == 'processing'
|
296
|
+
raise 'certificate timeout' if Time.now > timeout_time
|
297
|
+
|
298
|
+
sleep(1)
|
299
|
+
order.reload
|
300
|
+
end
|
301
|
+
|
302
|
+
# now write the certificate to file
|
303
|
+
|
304
|
+
# backup the old file if it exists.
|
305
|
+
|
306
|
+
if File.file?(File.join(ssl_dir, certificate_file))
|
307
|
+
backup_file(ssl_dir, certificate_file)
|
308
|
+
end
|
309
|
+
|
310
|
+
ssl_path = write_file(ssl_dir, certificate_file, order.certificate)
|
311
|
+
logger.info "ssl certificate has been written to :#{ssl_path}"
|
312
|
+
|
313
|
+
if hook_path
|
314
|
+
if system(hook_path)
|
315
|
+
logger.info "script :#{hook_path} succeeded"
|
316
|
+
else
|
317
|
+
logger.info "script :#{hook_path} failed !!"
|
318
|
+
end
|
319
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blix-letsencrypt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Clive Andrews
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: acme-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Command line utilities for letsencrypt
|
42
|
+
email:
|
43
|
+
- gems@realitybites.eu
|
44
|
+
executables:
|
45
|
+
- letsencrypt
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files:
|
48
|
+
- README.md
|
49
|
+
- LICENSE
|
50
|
+
files:
|
51
|
+
- LICENSE
|
52
|
+
- README.md
|
53
|
+
- bin/letsencrypt
|
54
|
+
- lib/blix/letsencrypt.rb
|
55
|
+
homepage: https://github.com/realbite/blix-letsencrypt
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.1.4
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: Command line utilities for managing letsencrypt ssl certificates
|
78
|
+
test_files: []
|