Skip to content

Commit 6e1ea73

Browse files
committed
Track Let's Encrypt certificate failures per-domain (ACME v2 rate limit)
This implements the minimal functionality for considering a domain's past certificate failures inside allow_domain. We believe something like this to be necessary to be able to comply to the new ACME v2 limit of 300 new orders/account/3h. A different approach which might be worth considering is saving the statistical information to (permanent) storage, like we do with certificates. This patch instead only uses a shm-based dictionary, meaning that information might be stale in a multi-server setup. (Dictionary entries are created with an expiration date, and we're running the following patch to look for a certificate in storage before calling allow_domain, meaning that this shouldn't be a problem in practice: Cargo@b1f9715)
1 parent c89e4a7 commit 6e1ea73

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-0
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,61 @@ auto_ssl:set("allow_domain", function(domain, auto_ssl, ssl_options, renewal)
161161
end)
162162
```
163163

164+
#### `get_failures`
165+
166+
The optional `get_failures` function accepts a domain name argument, and can be used to retrieve statistics about failed certificate requests concerning the domain. The function will return a table with fields `first` (timestamp of first failure encountered), `last` (timestamp of most recent failure encountered), `num` (number of failures). The function will instead return `nil` if no error has been encountered.
167+
168+
Note: the statistics are only kept for as long as the nginx instance is running. There is no sharing across multiple servers (as in a load-balanced environment) implemented.
169+
170+
To make use of the `get_failures` function, add the following to the `http` configuration block:
171+
172+
```nginx
173+
lua_shared_dict auto_ssl_failures 1m;
174+
```
175+
176+
When this shm-based dictionary exists, `lua-resty-auto-ssl` will use it to update a record it keeps for the domain whenever a Let's Encrypt certificate request fails (for both new domains, as well as renewing ones). When a certificate request is successful, `lua-resty-auto-ssl` will delete the record it has for the domain, so that future invocations will return `nil`.
177+
178+
The `get_failures` function can be used inside `allow_domain` to implement per-domain rate-limiting, and similar rule sets.
179+
180+
*Example:*
181+
182+
```lua
183+
auto_ssl:set("allow_domain", function(domain, auto_ssl, ssl_options, renewal)
184+
local failures = auto_ssl:get_failures(domain)
185+
-- only attempt one certificate request per hour
186+
if not failures or 3600 < ngx.now() - failures["last"] then
187+
return true
188+
else
189+
return false
190+
end
191+
end)
192+
```
193+
194+
#### `track_failure`
195+
196+
The optional `track_failure` function accepts a domain name argument and records a failure for this domain. This can be used to avoid repeated lookups of a domain in `allow_domain`.
197+
198+
*Example:*
199+
200+
```lua
201+
auto_ssl:set("allow_domain", function(domain, auto_ssl, ssl_options, renewal)
202+
local failures = auto_ssl:get_failures(domain)
203+
-- only attempt one lookup or certificate request per hour
204+
if failures and ngx.now() - failures["last"] <= 3600 then
205+
return false
206+
end
207+
208+
local allow
209+
-- (external lookup to check domain, e.g. via http)
210+
if not allow then
211+
auto_ssl:track_failure(domain)
212+
return false
213+
else
214+
return true
215+
end
216+
end)
217+
```
218+
164219
### `dir`
165220
*Default:* `/etc/resty-auto-ssl`
166221

lib/resty/auto-ssl.lua

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,62 @@ function _M.hook_server(self)
9999
server(self)
100100
end
101101

102+
function _M.get_failures(self, domain)
103+
if not ngx.shared.auto_ssl_failures then
104+
ngx.log(ngx.ERR, "auto-ssl: dict auto_ssl_failures could not be found. Please add it to your configuration: `lua_shared_dict auto_ssl_failures 1m;`")
105+
return
106+
end
107+
108+
local string = ngx.shared.auto_ssl_failures:get("domain:" .. domain)
109+
if string then
110+
local failures, json_err = self.storage.json_adapter:decode(string)
111+
if json_err then
112+
ngx.log(ngx.ERR, json_err, domain)
113+
end
114+
if failures then
115+
local mt = {
116+
__concat = function(op1, op2)
117+
return tostring(op1) .. tostring(op2)
118+
end,
119+
__tostring = function(f)
120+
return "first: " .. f["first"] .. ", last: " .. f["last"] .. ", num: " .. f["num"]
121+
end
122+
}
123+
setmetatable(failures, mt)
124+
return failures
125+
end
126+
end
127+
end
128+
129+
function _M.track_failure(self, domain)
130+
if not ngx.shared.auto_ssl_failures then
131+
return
132+
end
133+
134+
local failures
135+
local string = ngx.shared.auto_ssl_failures:get("domain:" .. domain)
136+
if string then
137+
failures = self.storage.json_adapter:decode(string)
138+
end
139+
if not failures then
140+
failures = {}
141+
failures["first"] = ngx.now()
142+
failures["last"] = failures["first"]
143+
failures["num"] = 1
144+
else
145+
failures["last"] = ngx.now()
146+
failures["num"] = failures["num"] + 1
147+
end
148+
string = self.storage.json_adapter:encode(failures)
149+
ngx.shared.auto_ssl_failures:set("domain:" .. domain, string, 2592000)
150+
end
151+
152+
function _M.track_success(_, domain)
153+
if not ngx.shared.auto_ssl_failures then
154+
return
155+
end
156+
157+
ngx.shared.auto_ssl_failures:delete("domain:" .. domain)
158+
end
159+
102160
return _M

lib/resty/auto-ssl/jobs/renewal.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ local function renew_check_cert(auto_ssl_instance, storage, domain)
183183
ngx.log(ngx.WARN, "auto-ssl: existing certificate is expired, deleting: ", domain)
184184
storage:delete_cert(domain)
185185
end
186+
187+
auto_ssl_instance:track_failure(domain)
188+
else
189+
auto_ssl_instance:track_success(domain)
186190
end
187191

188192
renew_check_cert_unlock(domain, storage, local_lock, distributed_lock_value)

lib/resty/auto-ssl/ssl_certificate.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ local function issue_cert(auto_ssl_instance, storage, domain)
9595
cert, err = ssl_provider.issue_cert(auto_ssl_instance, domain)
9696
if err then
9797
ngx.log(ngx.ERR, "auto-ssl: issuing new certificate failed: ", err)
98+
auto_ssl_instance:track_failure(domain)
99+
else
100+
auto_ssl_instance:track_success(domain)
98101
end
99102

100103
issue_cert_unlock(domain, storage, local_lock, distributed_lock_value)

0 commit comments

Comments
 (0)