blix-letsencrypt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ #!/env/ruby
2
+
3
+ require_relative '../lib/blix/letsencrypt'
@@ -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: []