rails-letsencrypt 0.12.0 → 0.13.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 +4 -4
- data/README.md +73 -4
- data/app/jobs/lets_encrypt/renew_certificates_job.rb +7 -2
- data/app/models/lets_encrypt/certificate.rb +54 -15
- data/lib/generators/lets_encrypt/templates/letsencrypt.rb +11 -1
- data/lib/letsencrypt/configuration.rb +10 -0
- data/lib/letsencrypt/errors.rb +8 -0
- data/lib/letsencrypt/issue_service.rb +46 -0
- data/lib/letsencrypt/renew_service.rb +25 -0
- data/lib/letsencrypt/status_checker.rb +26 -0
- data/lib/letsencrypt/verify_service.rb +46 -0
- data/lib/letsencrypt/version.rb +1 -1
- data/lib/letsencrypt.rb +10 -4
- data/lib/tasks/letsencrypt_tasks.rake +10 -7
- metadata +7 -6
- data/MIT-LICENSE +0 -20
- data/app/models/concerns/lets_encrypt/certificate_issuable.rb +0 -43
- data/app/models/concerns/lets_encrypt/certificate_verifiable.rb +0 -68
- data/lib/letsencrypt/logger_proxy.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0bcd52d0b1c063971fe7db79ea96162cdb3895bc8992b190de25773a5d6f3a59
|
4
|
+
data.tar.gz: 66ef7517093ba680cd0bdffbdf7dfc2e21f4822a29d41c9d3ba171e2f3cb4299
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 643bfad8030d1a7962e49ae7a85b354c3c7e13737d3e2e2ce5b9657c0964e20f3faf867cd51b23a2a695d0e1e30e66d7d54a3e125d135b65f2527447fc2d14f9
|
7
|
+
data.tar.gz: c37a4c9465a91717846aa952510fd7e37ab006732bb35076c50d3057b7ae28aadd70f0acc12a9c7e9d396a4bafd831762d77e06bb03d74ce22cfccf31fda086b
|
data/README.md
CHANGED
@@ -45,9 +45,13 @@ Add a file to `config/initializers/letsencrypt.rb` and put below config you need
|
|
45
45
|
|
46
46
|
```ruby
|
47
47
|
LetsEncrypt.config do |config|
|
48
|
+
# Configure the ACME server
|
49
|
+
# Default is Let's Encrypt production server
|
50
|
+
# config.acme_server = 'https://acme-v02.api.letsencrypt.org/directory'
|
51
|
+
|
48
52
|
# Using Let's Encrypt staging server or not
|
49
53
|
# Default only `Rails.env.production? == true` will use Let's Encrypt production server.
|
50
|
-
config.use_staging = true
|
54
|
+
# config.use_staging = true
|
51
55
|
|
52
56
|
# Set the private key path
|
53
57
|
# Default is locate at config/letsencrypt.key
|
@@ -70,13 +74,56 @@ LetsEncrypt.config do |config|
|
|
70
74
|
# Enable it if you want to customize the model
|
71
75
|
# Default is LetsEncrypt::Certificate
|
72
76
|
# config.certificate_model = 'MyCertificate'
|
77
|
+
|
78
|
+
# Configure the maximum attempts to re-check status when verifying or issuing
|
79
|
+
# config.max_attempts = 30
|
80
|
+
|
81
|
+
# Configure the interval between attempts
|
82
|
+
# config.retry_interval = 1
|
73
83
|
end
|
74
84
|
```
|
75
85
|
|
86
|
+
> [!WARNING]
|
87
|
+
> **Depcrecation Notice**
|
88
|
+
> The `use_staging` will be removed in the future, and the `acme_server` will be used to determine the server.
|
89
|
+
|
76
90
|
## Usage
|
77
91
|
|
78
92
|
The SSL certificate setup depends on the web server, this gem can work with `ngx_mruby` or `kong`.
|
79
93
|
|
94
|
+
### Service
|
95
|
+
|
96
|
+
#### Renew Service
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
certificate = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
100
|
+
|
101
|
+
service = LetsEncrypt::RenewService.new
|
102
|
+
service.execute(certificate)
|
103
|
+
```
|
104
|
+
|
105
|
+
#### Verify Service
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
certificate = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
109
|
+
|
110
|
+
order = LetsEncrypt.client.new_order(identifiers: [certificate.domain])
|
111
|
+
|
112
|
+
service = LetsEncrypt::VerifyService.new
|
113
|
+
service.execute(certificate, order)
|
114
|
+
```
|
115
|
+
|
116
|
+
#### Issue Service
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
certificate = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
120
|
+
|
121
|
+
order = LetsEncrypt.client.new_order(identifiers: [certificate.domain])
|
122
|
+
|
123
|
+
service = LetsEncrypt::IssueService.new
|
124
|
+
service.execute(certificate, order)
|
125
|
+
```
|
126
|
+
|
80
127
|
### Certificate Model
|
81
128
|
|
82
129
|
#### Create
|
@@ -88,6 +135,10 @@ cert = LetsEncrypt::Certificate.create(domain: 'example.com')
|
|
88
135
|
cert.get # alias `verify && issue`
|
89
136
|
```
|
90
137
|
|
138
|
+
> [!WARNING]
|
139
|
+
> **Depcrecation Notice**
|
140
|
+
> The `get` will be replaced by `RenewService` in the future.
|
141
|
+
|
91
142
|
#### Verify
|
92
143
|
|
93
144
|
Makes a request to Let's Encrypt and verify domain
|
@@ -97,6 +148,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
|
97
148
|
cert.verify
|
98
149
|
```
|
99
150
|
|
151
|
+
> [!WARNING]
|
152
|
+
> **Depcrecation Notice**
|
153
|
+
> The `verify` will be replaced by `VerifyService` in the future.
|
154
|
+
|
100
155
|
#### Issue
|
101
156
|
|
102
157
|
Ask Let's Encrypt to issue a new certificate.
|
@@ -106,6 +161,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
|
106
161
|
cert.issue
|
107
162
|
```
|
108
163
|
|
164
|
+
> [!WARNING]
|
165
|
+
> **Depcrecation Notice**
|
166
|
+
> The `issue` will be replaced by `IssueService` in the future.
|
167
|
+
|
109
168
|
#### Renew
|
110
169
|
|
111
170
|
```ruby
|
@@ -113,6 +172,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
|
113
172
|
cert.renew
|
114
173
|
```
|
115
174
|
|
175
|
+
> [!WARNING]
|
176
|
+
> **Depcrecation Notice**
|
177
|
+
> The `renew` will be replaced by `RenewService` in the future.
|
178
|
+
|
116
179
|
#### Status
|
117
180
|
|
118
181
|
Check a certificate is verified and issued.
|
@@ -141,7 +204,7 @@ rake letsencrypt:renew
|
|
141
204
|
|
142
205
|
If you are using Sidekiq or others, you can enqueue renew task daily.
|
143
206
|
|
144
|
-
```
|
207
|
+
```ruby
|
145
208
|
LetsEncrypt::RenewCertificatesJob.perform_later
|
146
209
|
```
|
147
210
|
|
@@ -150,11 +213,17 @@ LetsEncrypt::RenewCertificatesJob.perform_later
|
|
150
213
|
When the certificate is trying to issue a new one, you can subscribe it for logging or error handling.
|
151
214
|
|
152
215
|
```ruby
|
153
|
-
ActiveSupport::Notifications.subscribe('letsencrypt.
|
154
|
-
Rails.logger.info("Certificate for #{payload[:domain]} is
|
216
|
+
ActiveSupport::Notifications.subscribe('letsencrypt.renew') do |name, start, finish, id, payload|
|
217
|
+
Rails.logger.info("Certificate for #{payload[:domain]} is renewed")
|
155
218
|
end
|
156
219
|
```
|
157
220
|
|
221
|
+
The available events are:
|
222
|
+
|
223
|
+
* `letsencrypt.renew`
|
224
|
+
* `letsencrypt.verify`
|
225
|
+
* `letsencrypt.issue`
|
226
|
+
|
158
227
|
### ngx_mruby
|
159
228
|
|
160
229
|
The setup is following this [Article](http://hb.matsumoto-r.jp/entry/2017/03/23/173236)
|
@@ -6,10 +6,15 @@ module LetsEncrypt
|
|
6
6
|
queue_as :default
|
7
7
|
|
8
8
|
def perform
|
9
|
-
LetsEncrypt.
|
10
|
-
next if certificate.renew
|
9
|
+
service = LetsEncrypt::RenewService.new
|
11
10
|
|
11
|
+
LetsEncrypt.certificate_model.renewable.each do |certificate|
|
12
|
+
service.execute(certificate)
|
13
|
+
rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus
|
14
|
+
certificate.update(renew_after: 1.day.from_now)
|
15
|
+
rescue Acme::Client::Error => e
|
12
16
|
certificate.update(renew_after: 1.day.from_now)
|
17
|
+
Rails.logger.error("LetsEncrypt::RenewCertificatesJob: #{e.message}")
|
13
18
|
end
|
14
19
|
end
|
15
20
|
end
|
@@ -23,9 +23,6 @@ module LetsEncrypt
|
|
23
23
|
# index_letsencrypt_certificates_on_renew_after (renew_after)
|
24
24
|
#
|
25
25
|
class Certificate < ApplicationRecord
|
26
|
-
include CertificateVerifiable
|
27
|
-
include CertificateIssuable
|
28
|
-
|
29
26
|
self.table_name = 'letsencrypt_certificates'
|
30
27
|
|
31
28
|
validates :domain, presence: true, uniqueness: true
|
@@ -51,15 +48,6 @@ module LetsEncrypt
|
|
51
48
|
Time.zone.now >= expires_at
|
52
49
|
end
|
53
50
|
|
54
|
-
# Returns true if success get a new certificate
|
55
|
-
def get
|
56
|
-
ActiveSupport::Notifications.instrument('letsencrypt.issue', domain:) do
|
57
|
-
verify && issue
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
alias renew get
|
62
|
-
|
63
51
|
# Returns full-chain bundled certificates
|
64
52
|
def bundle
|
65
53
|
(certificate || '') + (intermediaries || '')
|
@@ -73,6 +61,22 @@ module LetsEncrypt
|
|
73
61
|
@key_object ||= OpenSSL::PKey::RSA.new(key)
|
74
62
|
end
|
75
63
|
|
64
|
+
def challenge!(filename, file_content)
|
65
|
+
update!(
|
66
|
+
verification_path: filename,
|
67
|
+
verification_string: file_content
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def refresh!(cert, fullchain)
|
72
|
+
update!(
|
73
|
+
certificate: cert.to_pem,
|
74
|
+
intermediaries: fullchain.join("\n\n"),
|
75
|
+
expires_at: cert.not_after,
|
76
|
+
renew_after: (cert.not_after - 1.month) + rand(10).days
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
76
80
|
# Save certificate into redis
|
77
81
|
def save_to_redis
|
78
82
|
LetsEncrypt::Redis.save(self)
|
@@ -83,12 +87,47 @@ module LetsEncrypt
|
|
83
87
|
LetsEncrypt::Redis.delete(self)
|
84
88
|
end
|
85
89
|
|
86
|
-
|
90
|
+
# Returns true if success get a new certificate
|
91
|
+
def get
|
92
|
+
logger.info "Getting certificate for #{domain}"
|
93
|
+
service = LetsEncrypt::RenewService.new
|
94
|
+
service.execute(self)
|
95
|
+
logger.info "Certificate issued for #{domain} " \
|
96
|
+
"(expires on #{expires_at}, will renew after #{renew_after})"
|
97
|
+
|
98
|
+
true
|
99
|
+
rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
|
100
|
+
logger.error "#{domain}: #{e.message}"
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
alias renew get
|
87
105
|
|
88
|
-
def
|
89
|
-
LetsEncrypt.
|
106
|
+
def verify
|
107
|
+
service = LetsEncrypt::VerifyService.new
|
108
|
+
service.execute(self, order)
|
109
|
+
|
110
|
+
true
|
111
|
+
rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
|
112
|
+
logger.error "#{domain}: #{e.message}"
|
113
|
+
false
|
114
|
+
end
|
115
|
+
|
116
|
+
def issue
|
117
|
+
logger.info "Getting certificate for #{domain}"
|
118
|
+
service = LetsEncrypt::IssueService.new
|
119
|
+
service.execute(self, order)
|
120
|
+
logger.info "Certificate issued for #{domain} " \
|
121
|
+
"(expires on #{expires_at}, will renew after #{renew_after})"
|
122
|
+
|
123
|
+
true
|
124
|
+
rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
|
125
|
+
logger.error "#{domain}: #{e.message}"
|
126
|
+
false
|
90
127
|
end
|
91
128
|
|
129
|
+
protected
|
130
|
+
|
92
131
|
def order
|
93
132
|
@order ||= LetsEncrypt.client.new_order(identifiers: [domain])
|
94
133
|
end
|
@@ -1,7 +1,11 @@
|
|
1
1
|
LetsEncrypt.config do |config|
|
2
|
+
# Configure the ACME server
|
3
|
+
# Default is Let's Encrypt production server
|
4
|
+
# config.acme_server = 'https://acme-v02.api.letsencrypt.org/directory'
|
5
|
+
|
2
6
|
# Using Let's Encrypt staging server or not
|
3
7
|
# Default only `Rails.env.production? == true` will use Let's Encrypt production server.
|
4
|
-
config.use_staging = true
|
8
|
+
# config.use_staging = true
|
5
9
|
|
6
10
|
# Set the private key path
|
7
11
|
# Default is locate at config/letsencrypt.key
|
@@ -24,5 +28,11 @@ LetsEncrypt.config do |config|
|
|
24
28
|
# Enable it if you want to customize the model
|
25
29
|
# Default is LetsEncrypt::Certificate
|
26
30
|
# config.certificate_model = 'MyCertificate'
|
31
|
+
|
32
|
+
# Configure the maximum attempts to re-check status when verifying or issuing
|
33
|
+
# config.max_attempts = 30
|
34
|
+
|
35
|
+
# Configure the interval between attempts
|
36
|
+
# config.retry_interval = 1
|
27
37
|
end
|
28
38
|
|
@@ -5,6 +5,8 @@ module LetsEncrypt
|
|
5
5
|
class Configuration
|
6
6
|
include ActiveSupport::Configurable
|
7
7
|
|
8
|
+
config_accessor :acme_directory
|
9
|
+
|
8
10
|
config_accessor :use_staging do
|
9
11
|
!Rails.env.production?
|
10
12
|
end
|
@@ -20,6 +22,14 @@ module LetsEncrypt
|
|
20
22
|
'LetsEncrypt::Certificate'
|
21
23
|
end
|
22
24
|
|
25
|
+
config_accessor :max_attempts do
|
26
|
+
30
|
27
|
+
end
|
28
|
+
|
29
|
+
config_accessor :retry_interval do
|
30
|
+
1
|
31
|
+
end
|
32
|
+
|
23
33
|
# Returns true if enabled `save_to_redis` feature
|
24
34
|
def use_redis?
|
25
35
|
save_to_redis == true
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LetsEncrypt
|
4
|
+
# The issue service to download the certificate
|
5
|
+
class IssueService
|
6
|
+
attr_reader :checker
|
7
|
+
|
8
|
+
STATUS_PROCESSING = 'processing'
|
9
|
+
|
10
|
+
def initialize(config: LetsEncrypt.config)
|
11
|
+
@checker = StatusChecker.new(
|
12
|
+
max_attempts: config.max_attempts,
|
13
|
+
interval: config.retry_interval
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(certificate, order)
|
18
|
+
ActiveSupport::Notifications.instrument('letsencrypt.issue', domain: certificate.domain) do
|
19
|
+
issue(certificate, order)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def issue(certificate, order)
|
26
|
+
csr = build_csr(certificate)
|
27
|
+
order.finalize(csr:)
|
28
|
+
checker.execute do
|
29
|
+
order.reload
|
30
|
+
order.status != STATUS_PROCESSING
|
31
|
+
end
|
32
|
+
fullchain = order.certificate.split("\n\n")
|
33
|
+
cert = OpenSSL::X509::Certificate.new(fullchain.shift)
|
34
|
+
certificate.refresh!(cert, fullchain)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_csr(certificate)
|
38
|
+
Acme::Client::CertificateRequest.new(
|
39
|
+
private_key: OpenSSL::PKey::RSA.new(certificate.key),
|
40
|
+
subject: {
|
41
|
+
common_name: certificate.domain
|
42
|
+
}
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LetsEncrypt
|
4
|
+
# The renew service to create or renew the certificate
|
5
|
+
class RenewService
|
6
|
+
attr_reader :acme_client, :config
|
7
|
+
|
8
|
+
def initialize(acme_client: LetsEncrypt.client, config: LetsEncrypt.config)
|
9
|
+
@acme_client = acme_client
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(certificate)
|
14
|
+
ActiveSupport::Notifications.instrument('letsencrypt.renew', domain: certificate.domain) do
|
15
|
+
order = acme_client.new_order(identifiers: [certificate.domain])
|
16
|
+
|
17
|
+
verify_service = VerifyService.new(config:)
|
18
|
+
verify_service.execute(certificate, order)
|
19
|
+
|
20
|
+
issue_service = IssueService.new(config:)
|
21
|
+
issue_service.execute(certificate, order)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LetsEncrypt
|
4
|
+
# The status checker to make a loop until the status is reached
|
5
|
+
class StatusChecker
|
6
|
+
attr_reader :max_attempts, :interval
|
7
|
+
|
8
|
+
def initialize(max_attempts:, interval:)
|
9
|
+
@max_attempts = max_attempts
|
10
|
+
@interval = interval
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
attempts = 0
|
15
|
+
|
16
|
+
loop do
|
17
|
+
break if yield
|
18
|
+
|
19
|
+
attempts += 1
|
20
|
+
raise MaxCheckExceeded, "Max attempts exceeded (#{max_attempts})" if attempts >= max_attempts
|
21
|
+
|
22
|
+
sleep interval
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LetsEncrypt
|
4
|
+
# Process the verification of the domain
|
5
|
+
class VerifyService
|
6
|
+
STATUS_PENDING = 'pending'
|
7
|
+
STATUS_VALID = 'valid'
|
8
|
+
|
9
|
+
attr_reader :checker
|
10
|
+
|
11
|
+
def initialize(config: LetsEncrypt.config)
|
12
|
+
@checker = StatusChecker.new(
|
13
|
+
max_attempts: config.max_attempts,
|
14
|
+
interval: config.retry_interval
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute(certificate, order)
|
19
|
+
ActiveSupport::Notifications.instrument('letsencrypt.verify', domain: certificate.domain) do
|
20
|
+
verify(certificate, order)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def verify(certificate, order)
|
27
|
+
challenge = order.authorizations.first.http
|
28
|
+
|
29
|
+
certificate.challenge!(challenge.filename, challenge.file_content)
|
30
|
+
|
31
|
+
challenge.request_validation
|
32
|
+
|
33
|
+
checker.execute do
|
34
|
+
challenge.reload
|
35
|
+
challenge.status != STATUS_PENDING
|
36
|
+
end
|
37
|
+
assert(challenge)
|
38
|
+
end
|
39
|
+
|
40
|
+
def assert(challenge)
|
41
|
+
return if challenge.status == STATUS_VALID
|
42
|
+
|
43
|
+
raise LetsEncrypt::InvalidStatus, "Status not valid (was: #{challenge.status})"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/letsencrypt/version.rb
CHANGED
data/lib/letsencrypt.rb
CHANGED
@@ -5,9 +5,13 @@ require 'acme-client'
|
|
5
5
|
require 'redis'
|
6
6
|
require 'letsencrypt/railtie'
|
7
7
|
require 'letsencrypt/engine'
|
8
|
+
require 'letsencrypt/errors'
|
8
9
|
require 'letsencrypt/configuration'
|
9
|
-
require 'letsencrypt/logger_proxy'
|
10
10
|
require 'letsencrypt/redis'
|
11
|
+
require 'letsencrypt/status_checker'
|
12
|
+
require 'letsencrypt/verify_service'
|
13
|
+
require 'letsencrypt/issue_service'
|
14
|
+
require 'letsencrypt/renew_service'
|
11
15
|
|
12
16
|
# :nodoc:
|
13
17
|
module LetsEncrypt
|
@@ -23,7 +27,8 @@ module LetsEncrypt
|
|
23
27
|
def client
|
24
28
|
@client ||= ::Acme::Client.new(
|
25
29
|
private_key:,
|
26
|
-
directory
|
30
|
+
directory:,
|
31
|
+
bad_nonce_retry: 5
|
27
32
|
)
|
28
33
|
end
|
29
34
|
|
@@ -40,7 +45,8 @@ module LetsEncrypt
|
|
40
45
|
|
41
46
|
# Get current using Let's Encrypt endpoint
|
42
47
|
def directory
|
43
|
-
@directory ||= config.
|
48
|
+
@directory ||= config.acme_directory ||
|
49
|
+
(config.use_staging? ? ENDPOINT_STAGING : ENDPOINT)
|
44
50
|
end
|
45
51
|
|
46
52
|
# Register a Let's Encrypt account
|
@@ -68,7 +74,7 @@ module LetsEncrypt
|
|
68
74
|
end
|
69
75
|
|
70
76
|
def logger
|
71
|
-
@logger ||=
|
77
|
+
@logger ||= Rails.logger.tagged('LetsEncrypt')
|
72
78
|
end
|
73
79
|
|
74
80
|
# Config how to Let's Encrypt works for Rails
|
@@ -3,18 +3,21 @@
|
|
3
3
|
namespace :letsencrypt do
|
4
4
|
desc 'Renew certificates that already expired or expiring soon'
|
5
5
|
task renew: :environment do
|
6
|
-
|
6
|
+
success = 0
|
7
7
|
failed = 0
|
8
8
|
|
9
|
-
LetsEncrypt.
|
10
|
-
count += 1
|
11
|
-
|
12
|
-
next if certificate.renew
|
9
|
+
service = LetsEncrypt::RenewService.new
|
13
10
|
|
11
|
+
LetsEncrypt.certificate_model.renewable.each do |certificate|
|
12
|
+
service.execute(certificate)
|
13
|
+
success += 1
|
14
|
+
rescue Acme::Client::Error, LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
|
14
15
|
failed += 1
|
15
|
-
puts "Could not renew domain: #{certificate.domain}"
|
16
|
+
puts "Could not renew domain: #{certificate.domain} - #{e.message}"
|
16
17
|
end
|
17
18
|
|
18
|
-
puts "Renewed #{
|
19
|
+
puts "Renewed #{success} certificates successfully."
|
20
|
+
puts "Failed to renew #{failed} certificates."
|
21
|
+
puts "Total: #{success + failed} certificates."
|
19
22
|
end
|
20
23
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-letsencrypt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 蒼時弦也
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-05-
|
10
|
+
date: 2025-05-20 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: acme-client
|
@@ -142,15 +142,12 @@ executables: []
|
|
142
142
|
extensions: []
|
143
143
|
extra_rdoc_files: []
|
144
144
|
files:
|
145
|
-
- MIT-LICENSE
|
146
145
|
- README.md
|
147
146
|
- Rakefile
|
148
147
|
- app/controllers/lets_encrypt/application_controller.rb
|
149
148
|
- app/controllers/lets_encrypt/verifications_controller.rb
|
150
149
|
- app/jobs/lets_encrypt/application_job.rb
|
151
150
|
- app/jobs/lets_encrypt/renew_certificates_job.rb
|
152
|
-
- app/models/concerns/lets_encrypt/certificate_issuable.rb
|
153
|
-
- app/models/concerns/lets_encrypt/certificate_verifiable.rb
|
154
151
|
- app/models/lets_encrypt/certificate.rb
|
155
152
|
- config/routes.rb
|
156
153
|
- lib/generators/lets_encrypt/install_generator.rb
|
@@ -160,9 +157,13 @@ files:
|
|
160
157
|
- lib/letsencrypt.rb
|
161
158
|
- lib/letsencrypt/configuration.rb
|
162
159
|
- lib/letsencrypt/engine.rb
|
163
|
-
- lib/letsencrypt/
|
160
|
+
- lib/letsencrypt/errors.rb
|
161
|
+
- lib/letsencrypt/issue_service.rb
|
164
162
|
- lib/letsencrypt/railtie.rb
|
165
163
|
- lib/letsencrypt/redis.rb
|
164
|
+
- lib/letsencrypt/renew_service.rb
|
165
|
+
- lib/letsencrypt/status_checker.rb
|
166
|
+
- lib/letsencrypt/verify_service.rb
|
166
167
|
- lib/letsencrypt/version.rb
|
167
168
|
- lib/rails-letsencrypt.rb
|
168
169
|
- lib/tasks/letsencrypt_tasks.rake
|
data/MIT-LICENSE
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
Copyright 2017 蒼時弦也
|
2
|
-
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
a copy of this software and associated documentation files (the
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
the following conditions:
|
10
|
-
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
included in all copies or substantial portions of the Software.
|
13
|
-
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module LetsEncrypt
|
4
|
-
# :nodoc:
|
5
|
-
module CertificateIssuable
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
# Returns true if issue new certificate succeed.
|
9
|
-
def issue
|
10
|
-
logger.info "Getting certificate for #{domain}"
|
11
|
-
create_certificate
|
12
|
-
logger.info "Certificate issued for #{domain} (expires on #{expires_at}, will renew after #{renew_after})"
|
13
|
-
true
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
def csr
|
19
|
-
Acme::Client::CertificateRequest.new(
|
20
|
-
private_key: OpenSSL::PKey::RSA.new(key),
|
21
|
-
subject: {
|
22
|
-
common_name: domain
|
23
|
-
}
|
24
|
-
)
|
25
|
-
end
|
26
|
-
|
27
|
-
def create_certificate
|
28
|
-
order.finalize(csr:)
|
29
|
-
sleep 1 while order.status == 'processing'
|
30
|
-
fullchain = order.certificate.split("\n\n")
|
31
|
-
assign_new_certificate(fullchain)
|
32
|
-
save!
|
33
|
-
end
|
34
|
-
|
35
|
-
def assign_new_certificate(fullchain)
|
36
|
-
cert = OpenSSL::X509::Certificate.new(fullchain.shift)
|
37
|
-
self.certificate = cert.to_pem
|
38
|
-
self.intermediaries = fullchain.join("\n\n")
|
39
|
-
self.expires_at = cert.not_after
|
40
|
-
self.renew_after = (expires_at - 1.month) + rand(10).days
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module LetsEncrypt
|
4
|
-
# :nodoc:
|
5
|
-
module CertificateVerifiable
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
# Returns true if verify domain is succeed.
|
9
|
-
def verify
|
10
|
-
create_order
|
11
|
-
start_challenge
|
12
|
-
wait_verify_status
|
13
|
-
check_verify_status
|
14
|
-
rescue Acme::Client::Error => e
|
15
|
-
retry_on_verify_error(e)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def create_order
|
21
|
-
# TODO: Support multiple domain
|
22
|
-
@challenge = order.authorizations.first.http
|
23
|
-
self.verification_path = @challenge.filename
|
24
|
-
self.verification_string = @challenge.file_content
|
25
|
-
save!
|
26
|
-
end
|
27
|
-
|
28
|
-
def start_challenge
|
29
|
-
logger.info "Attempting verification of #{domain}"
|
30
|
-
@challenge.request_validation
|
31
|
-
end
|
32
|
-
|
33
|
-
def wait_verify_status
|
34
|
-
checks = 0
|
35
|
-
until @challenge.status != 'pending'
|
36
|
-
checks += 1
|
37
|
-
if checks > 30
|
38
|
-
logger.info "#{domain}: Status remained at pending for 30 checks"
|
39
|
-
return false
|
40
|
-
end
|
41
|
-
sleep 1
|
42
|
-
@challenge.reload
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def check_verify_status
|
47
|
-
unless @challenge.status == 'valid'
|
48
|
-
logger.info "#{domain}: Status was not valid (was: #{@challenge.status})"
|
49
|
-
return false
|
50
|
-
end
|
51
|
-
|
52
|
-
true
|
53
|
-
end
|
54
|
-
|
55
|
-
def retry_on_verify_error(error)
|
56
|
-
@retries ||= 0
|
57
|
-
if error.is_a?(Acme::Client::Error::BadNonce) && @retries < 5
|
58
|
-
@retries += 1
|
59
|
-
logger.info "#{domain}: Bad nounce encountered. Retrying (#{@retries} of 5 attempts)"
|
60
|
-
sleep 1
|
61
|
-
verify
|
62
|
-
else
|
63
|
-
logger.info "#{domain}: Error: #{error.class} (#{error.message})"
|
64
|
-
false
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module LetsEncrypt
|
4
|
-
# :nodoc:
|
5
|
-
class LoggerProxy
|
6
|
-
attr_reader :tags
|
7
|
-
|
8
|
-
def initialize(logger, tags:)
|
9
|
-
@logger = logger
|
10
|
-
@tags = tags.flatten
|
11
|
-
end
|
12
|
-
|
13
|
-
def add_tags(*tags)
|
14
|
-
@tags += tags.flatten
|
15
|
-
@tags = @tags.uniq
|
16
|
-
end
|
17
|
-
|
18
|
-
def tag(logger, &)
|
19
|
-
if logger.respond_to?(:tagged)
|
20
|
-
current_tags = tags - logger.formatter.current_tags
|
21
|
-
logger.tagged(*current_tags, &)
|
22
|
-
else
|
23
|
-
yield
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
%i[debug info warn error fatal unknown].each do |severity|
|
28
|
-
define_method(severity) do |message|
|
29
|
-
log severity, message
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def log(type, message)
|
36
|
-
tag(@logger) { @logger.send type, message }
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|