rails-letsencrypt 0.11.3 → 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 +82 -12
- data/app/jobs/lets_encrypt/renew_certificates_job.rb +7 -2
- data/app/models/lets_encrypt/certificate.rb +55 -16
- data/lib/generators/lets_encrypt/install_generator.rb +4 -0
- data/lib/generators/lets_encrypt/templates/letsencrypt.rb +38 -0
- 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 +13 -7
- data/lib/tasks/letsencrypt_tasks.rake +10 -7
- metadata +10 -11
- 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
@@ -1,11 +1,16 @@
|
|
1
|
-
|
1
|
+
Rails LetsEncrypt
|
2
|
+
===
|
3
|
+
|
4
|
+
[](https://badge.fury.io/rb/rails-letsencrypt)
|
5
|
+
[](https://codeclimate.com/github/elct9620/rails-letsencrypt)
|
6
|
+
[](https://deepwiki.com/elct9620/rails-letsencrypt)
|
2
7
|
|
3
8
|
Provide manageable Let's Encrypt Certificate for Rails.
|
4
9
|
|
5
10
|
## Requirement
|
6
11
|
|
7
|
-
* Rails
|
8
|
-
* Ruby 2
|
12
|
+
* Rails 7.2+
|
13
|
+
* Ruby 3.2+
|
9
14
|
|
10
15
|
## Installation
|
11
16
|
|
@@ -40,9 +45,13 @@ Add a file to `config/initializers/letsencrypt.rb` and put below config you need
|
|
40
45
|
|
41
46
|
```ruby
|
42
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
|
+
|
43
52
|
# Using Let's Encrypt staging server or not
|
44
53
|
# Default only `Rails.env.production? == true` will use Let's Encrypt production server.
|
45
|
-
config.use_staging = true
|
54
|
+
# config.use_staging = true
|
46
55
|
|
47
56
|
# Set the private key path
|
48
57
|
# Default is locate at config/letsencrypt.key
|
@@ -64,14 +73,57 @@ LetsEncrypt.config do |config|
|
|
64
73
|
|
65
74
|
# Enable it if you want to customize the model
|
66
75
|
# Default is LetsEncrypt::Certificate
|
67
|
-
#config.certificate_model = 'MyCertificate'
|
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
|
68
83
|
end
|
69
84
|
```
|
70
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
|
+
|
71
90
|
## Usage
|
72
91
|
|
73
92
|
The SSL certificate setup depends on the web server, this gem can work with `ngx_mruby` or `kong`.
|
74
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
|
+
|
75
127
|
### Certificate Model
|
76
128
|
|
77
129
|
#### Create
|
@@ -83,6 +135,10 @@ cert = LetsEncrypt::Certificate.create(domain: 'example.com')
|
|
83
135
|
cert.get # alias `verify && issue`
|
84
136
|
```
|
85
137
|
|
138
|
+
> [!WARNING]
|
139
|
+
> **Depcrecation Notice**
|
140
|
+
> The `get` will be replaced by `RenewService` in the future.
|
141
|
+
|
86
142
|
#### Verify
|
87
143
|
|
88
144
|
Makes a request to Let's Encrypt and verify domain
|
@@ -92,6 +148,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
|
92
148
|
cert.verify
|
93
149
|
```
|
94
150
|
|
151
|
+
> [!WARNING]
|
152
|
+
> **Depcrecation Notice**
|
153
|
+
> The `verify` will be replaced by `VerifyService` in the future.
|
154
|
+
|
95
155
|
#### Issue
|
96
156
|
|
97
157
|
Ask Let's Encrypt to issue a new certificate.
|
@@ -101,6 +161,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
|
101
161
|
cert.issue
|
102
162
|
```
|
103
163
|
|
164
|
+
> [!WARNING]
|
165
|
+
> **Depcrecation Notice**
|
166
|
+
> The `issue` will be replaced by `IssueService` in the future.
|
167
|
+
|
104
168
|
#### Renew
|
105
169
|
|
106
170
|
```ruby
|
@@ -108,6 +172,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
|
|
108
172
|
cert.renew
|
109
173
|
```
|
110
174
|
|
175
|
+
> [!WARNING]
|
176
|
+
> **Depcrecation Notice**
|
177
|
+
> The `renew` will be replaced by `RenewService` in the future.
|
178
|
+
|
111
179
|
#### Status
|
112
180
|
|
113
181
|
Check a certificate is verified and issued.
|
@@ -136,7 +204,7 @@ rake letsencrypt:renew
|
|
136
204
|
|
137
205
|
If you are using Sidekiq or others, you can enqueue renew task daily.
|
138
206
|
|
139
|
-
```
|
207
|
+
```ruby
|
140
208
|
LetsEncrypt::RenewCertificatesJob.perform_later
|
141
209
|
```
|
142
210
|
|
@@ -145,11 +213,17 @@ LetsEncrypt::RenewCertificatesJob.perform_later
|
|
145
213
|
When the certificate is trying to issue a new one, you can subscribe it for logging or error handling.
|
146
214
|
|
147
215
|
```ruby
|
148
|
-
ActiveSupport::Notifications.subscribe('letsencrypt.
|
149
|
-
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")
|
150
218
|
end
|
151
219
|
```
|
152
220
|
|
221
|
+
The available events are:
|
222
|
+
|
223
|
+
* `letsencrypt.renew`
|
224
|
+
* `letsencrypt.verify`
|
225
|
+
* `letsencrypt.issue`
|
226
|
+
|
153
227
|
### ngx_mruby
|
154
228
|
|
155
229
|
The setup is following this [Article](http://hb.matsumoto-r.jp/entry/2017/03/23/173236)
|
@@ -200,9 +274,5 @@ server {
|
|
200
274
|
}
|
201
275
|
```
|
202
276
|
|
203
|
-
### Kong
|
204
|
-
|
205
|
-
Coming soon.
|
206
|
-
|
207
277
|
## License
|
208
278
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -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,16 +23,13 @@ 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
|
32
29
|
|
33
30
|
scope :active, -> { where('certificate IS NOT NULL AND expires_at > ?', Time.zone.now) }
|
34
31
|
scope :renewable, -> { where('renew_after IS NULL OR renew_after <= ?', Time.zone.now) }
|
35
|
-
scope :expired, -> { where(
|
32
|
+
scope :expired, -> { where(expires_at: ..Time.zone.now) }
|
36
33
|
|
37
34
|
before_create -> { self.key = OpenSSL::PKey::RSA.new(4096).to_s }
|
38
35
|
after_destroy -> { delete_from_redis }, if: -> { LetsEncrypt.config.use_redis? && active? }
|
@@ -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: 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
|
@@ -0,0 +1,38 @@
|
|
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
|
+
|
6
|
+
# Using Let's Encrypt staging server or not
|
7
|
+
# Default only `Rails.env.production? == true` will use Let's Encrypt production server.
|
8
|
+
# config.use_staging = true
|
9
|
+
|
10
|
+
# Set the private key path
|
11
|
+
# Default is locate at config/letsencrypt.key
|
12
|
+
config.private_key_path = Rails.root.join('config', 'letsencrypt.key')
|
13
|
+
|
14
|
+
# Use environment variable to set private key
|
15
|
+
# If enable, the API Client will use `LETSENCRYPT_PRIVATE_KEY` as private key
|
16
|
+
# Default is false
|
17
|
+
config.use_env_key = false
|
18
|
+
|
19
|
+
# Should sync certificate into redis
|
20
|
+
# When using ngx_mruby to dynamic load certificate, this will be helpful
|
21
|
+
# Default is false
|
22
|
+
config.save_to_redis = false
|
23
|
+
|
24
|
+
# The redis server url
|
25
|
+
# Default is nil
|
26
|
+
config.redis_url = 'redis://localhost:6379/1'
|
27
|
+
|
28
|
+
# Enable it if you want to customize the model
|
29
|
+
# Default is LetsEncrypt::Certificate
|
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
|
37
|
+
end
|
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
|
@@ -22,8 +26,9 @@ module LetsEncrypt
|
|
22
26
|
# Create the ACME Client to Let's Encrypt
|
23
27
|
def client
|
24
28
|
@client ||= ::Acme::Client.new(
|
25
|
-
private_key
|
26
|
-
directory
|
29
|
+
private_key:,
|
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
|
@@ -77,9 +83,9 @@ module LetsEncrypt
|
|
77
83
|
# # Always use production mode to connect Let's Encrypt API server
|
78
84
|
# config.use_staging = false
|
79
85
|
# end
|
80
|
-
def config(&
|
86
|
+
def config(&)
|
81
87
|
@config ||= Configuration.new
|
82
|
-
instance_exec(@config, &
|
88
|
+
instance_exec(@config, &) if block_given?
|
83
89
|
@config
|
84
90
|
end
|
85
91
|
|
@@ -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,14 +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
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-05-20 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: acme-client
|
@@ -143,26 +142,28 @@ executables: []
|
|
143
142
|
extensions: []
|
144
143
|
extra_rdoc_files: []
|
145
144
|
files:
|
146
|
-
- MIT-LICENSE
|
147
145
|
- README.md
|
148
146
|
- Rakefile
|
149
147
|
- app/controllers/lets_encrypt/application_controller.rb
|
150
148
|
- app/controllers/lets_encrypt/verifications_controller.rb
|
151
149
|
- app/jobs/lets_encrypt/application_job.rb
|
152
150
|
- app/jobs/lets_encrypt/renew_certificates_job.rb
|
153
|
-
- app/models/concerns/lets_encrypt/certificate_issuable.rb
|
154
|
-
- app/models/concerns/lets_encrypt/certificate_verifiable.rb
|
155
151
|
- app/models/lets_encrypt/certificate.rb
|
156
152
|
- config/routes.rb
|
157
153
|
- lib/generators/lets_encrypt/install_generator.rb
|
158
154
|
- lib/generators/lets_encrypt/register_generator.rb
|
155
|
+
- lib/generators/lets_encrypt/templates/letsencrypt.rb
|
159
156
|
- lib/generators/lets_encrypt/templates/migration.rb
|
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
|
@@ -171,7 +172,6 @@ licenses:
|
|
171
172
|
- MIT
|
172
173
|
metadata:
|
173
174
|
rubygems_mfa_required: 'true'
|
174
|
-
post_install_message:
|
175
175
|
rdoc_options: []
|
176
176
|
require_paths:
|
177
177
|
- lib
|
@@ -179,15 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
179
179
|
requirements:
|
180
180
|
- - ">="
|
181
181
|
- !ruby/object:Gem::Version
|
182
|
-
version: 2.
|
182
|
+
version: 3.2.0
|
183
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
184
|
requirements:
|
185
185
|
- - ">="
|
186
186
|
- !ruby/object:Gem::Version
|
187
187
|
version: '0'
|
188
188
|
requirements: []
|
189
|
-
rubygems_version: 3.
|
190
|
-
signing_key:
|
189
|
+
rubygems_version: 3.6.2
|
191
190
|
specification_version: 4
|
192
191
|
summary: The Let's Encrypt certificate manager for rails
|
193
192
|
test_files: []
|
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: 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, &block)
|
19
|
-
if logger.respond_to?(:tagged)
|
20
|
-
current_tags = tags - logger.formatter.current_tags
|
21
|
-
logger.tagged(*current_tags, &block)
|
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
|