acme_nsupdate 0.2.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.txt +21 -0
- data/README.md +36 -0
- data/bin/acme_nsupdate +6 -0
- data/lib/acme_nsupdate.rb +1 -0
- data/lib/acme_nsupdate/cli.rb +90 -0
- data/lib/acme_nsupdate/client.rb +186 -0
- data/lib/acme_nsupdate/nsupdate.rb +55 -0
- data/lib/acme_nsupdate/strategies/dns01.rb +54 -0
- data/lib/acme_nsupdate/strategies/http01.rb +46 -0
- data/lib/acme_nsupdate/strategy.rb +39 -0
- data/lib/acme_nsupdate/version.rb +3 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7c52e0be82cd56ce8fb4c5bcd48b1e29f42a4cb0
|
4
|
+
data.tar.gz: ae6a8c0739c5ac9a2eea8565930b3d67eb02e853
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d6a7904ea0ede7ef6abf02d175e7ffa6640399c0ed526a27a737d1ffcdfe1686003b73983b9dbc0f5e902e14a127aefdc9f80a8c59ee8d1e71a962f23a4b3ba0
|
7
|
+
data.tar.gz: 6b6067511ab4f30165182030da025b176b3b4d8da59dc686cb41de2c89edb8941f585182f9258a4c5f08f72febe8233b817e3d5e12e40b5fc808fcf836b2cee8
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Jonne Haß
|
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,36 @@
|
|
1
|
+
# ACME nsupdate [](https://rubygems.org/gems/acme_nsupdate)
|
2
|
+
|
3
|
+
ACME (Let's Encrypt) client with nsupdate (DDNS) integration.
|
4
|
+
|
5
|
+
CLI tool to obtain certificates via ACME and update the matching TLSA records.
|
6
|
+
The primary authentication method is http-01 via webroot for now, but dns-01 is supported too.
|
7
|
+
|
8
|
+
*Don't actually trust this, I wrote it for myself. Read and understand the code if you want to
|
9
|
+
actually use it. **There are no tests!***
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Install the gem:
|
14
|
+
|
15
|
+
```
|
16
|
+
$ gem install acme_nsupdate
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
See the help:
|
22
|
+
|
23
|
+
```
|
24
|
+
$ acme_nsupdate --help
|
25
|
+
```
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jhass/acme_nsupdate.
|
30
|
+
Feature contributions are welcome too, feature requests will most likely not be fulfilled.
|
31
|
+
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
36
|
+
|
data/bin/acme_nsupdate
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "acme_nsupdate/client"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "slop"
|
2
|
+
|
3
|
+
require "acme_nsupdate/version"
|
4
|
+
require "acme_nsupdate/client"
|
5
|
+
|
6
|
+
module AcmeNsupdate
|
7
|
+
class Cli
|
8
|
+
def initialize argv=ARGV
|
9
|
+
@options = Slop.parse(argv) do |o|
|
10
|
+
o.array "-d", "--domains", "The FQDNs to request a certificate for, multiple should be comma separated."
|
11
|
+
o.string "-m", "--master", "The nameserver to use to provision the TXT and TLSA records to. Defaults to the primary nameserver specifed in the SOA record."
|
12
|
+
o.string "-t", "--ttl", "The TTLs of the TXT and TLSA records created, separated by a comma. Defaults to 60,43200", default: "60,43200"
|
13
|
+
o.bool "-k", "--keep", "Skip removing any kind of temporary data after successfully obtaining the certificate."
|
14
|
+
o.string "-K", "--tsig", "TSIG key to use for DNS updates. Expected format is name:key."
|
15
|
+
o.string "-e", "--endpoint", "ACME API endpoint. Defaults to: https://acme-v01.api.letsencrypt.org", default: "https://acme-v01.api.letsencrypt.org"
|
16
|
+
o.string "-D", "--datadir", "Base directory for certificates and account keys. Defaults to: /etc/letsencrypt", default: "/etc/letsencrypt"
|
17
|
+
o.string "-c", "--contact", "Contact mail address."
|
18
|
+
o.integer "-l", "--keylength", "Length of the generated RSA keys. Defaults to 2048.", default: 2048
|
19
|
+
o.bool "-T", "--notlsa", "Do not publish TLSA records (publishing them drops all old ones). Defaults to no.", default: false
|
20
|
+
o.array "-p", "--tlsaports", "Ports to publish TLSA records for. A plain port publishes for all FQDNs given, fqdn:port publishes for a single FQDN, [fqdn1 fqdn2]:port publishes for a subset. Multiple values should be comma separated. Defaults to 443.", default: ["443"]
|
21
|
+
o.string "-C", "--challenge", "Challenge to use, either http-01 or dns-01. http-01 requires the webroot option. Defaults to http-01.", default: "http-01"
|
22
|
+
o.string "-w", "--webroot", "Webroot to save http-01 challenges to."
|
23
|
+
o.bool "-V", "--verbose", "Enable debug logging.", default: false
|
24
|
+
o.bool "-q", "--quiet", "Only print error messages.", default: false
|
25
|
+
o.bool "-f", "--force", "Force, even if cert is still valid.", default: false
|
26
|
+
|
27
|
+
o.on "-v", "--version", "Display version." do
|
28
|
+
puts "ACME nsupdate #{AcmeNsupdate::VERSION}"
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
o.on "-h", "--help", "Display this help." do
|
33
|
+
puts o
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
abort "Unexpected extra arguments #{@options.arguments}" unless @options.arguments.empty?
|
39
|
+
|
40
|
+
@options = @options.to_h
|
41
|
+
|
42
|
+
abort "You need to provide a domain!" unless domain_given?
|
43
|
+
abort "A domain was given more than once!" unless domains_unique?
|
44
|
+
abort "You need to provide a contact mail address!" unless contact_given?
|
45
|
+
abort "Invalid TSIG key: name or key missing!" unless valid_tsig?
|
46
|
+
abort "No webroot given or not writable!" unless valid_webroot?
|
47
|
+
abort "Invalid TTL specification" unless valid_ttl?
|
48
|
+
abort "Can't silence output and enable debug logging at the same time." unless valid_verbosity?
|
49
|
+
|
50
|
+
@options[:txt_ttl], @options[:tlsa_ttl] = @options[:ttl].split(",")
|
51
|
+
end
|
52
|
+
|
53
|
+
def run
|
54
|
+
Client.new(@options).run
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def domain_given?
|
60
|
+
!@options[:domains].empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def domains_unique?
|
64
|
+
@options[:domains] == @options[:domains].uniq
|
65
|
+
end
|
66
|
+
|
67
|
+
def contact_given?
|
68
|
+
!@options[:contact].nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid_tsig?
|
72
|
+
@options[:tsig].nil? ||
|
73
|
+
@options[:tsig].include?(":")
|
74
|
+
end
|
75
|
+
|
76
|
+
def valid_webroot?
|
77
|
+
@options[:challenge] != "http-01" ||
|
78
|
+
!@options[:webroot].nil? ||
|
79
|
+
File.writable?(@options[:webroot])
|
80
|
+
end
|
81
|
+
|
82
|
+
def valid_ttl?
|
83
|
+
!@options[:ttl][/\A\d+,\d+\z/].nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
def valid_verbosity?
|
87
|
+
!(@options[:verbose] && @options[:quiet])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "pathname"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
require "acme-client"
|
6
|
+
require "faraday/detailed_logger"
|
7
|
+
|
8
|
+
require "acme_nsupdate/strategy"
|
9
|
+
require "acme_nsupdate/nsupdate"
|
10
|
+
|
11
|
+
module AcmeNsupdate
|
12
|
+
class Client
|
13
|
+
class Error < RuntimeError
|
14
|
+
end
|
15
|
+
|
16
|
+
RENEWAL_THRESHOLD = 2_592_000 # 30*24*60*60, 30 days
|
17
|
+
|
18
|
+
attr_reader :options, :logger
|
19
|
+
|
20
|
+
def initialize options
|
21
|
+
@options = options
|
22
|
+
@logger = Logger.new(STDOUT)
|
23
|
+
@logger.level = Logger::INFO
|
24
|
+
@logger.level = Logger::FATAL if @options[:quiet]
|
25
|
+
@logger.level = Logger::DEBUG if @options[:verbose]
|
26
|
+
@verification_strategy = Strategy.for(@options[:challenge]).new(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
unless renewal_needed?
|
31
|
+
logger.info "Existing certificate is still valid long enough."
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
register_account
|
36
|
+
challenges = @verification_strategy.verify_domains
|
37
|
+
logger.info "Requesting certificate"
|
38
|
+
certificate = client.new_certificate csr
|
39
|
+
write_files live_path, certificate, private_key
|
40
|
+
write_files archive_path, certificate, private_key
|
41
|
+
@verification_strategy.cleanup challenges unless @options[:keep]
|
42
|
+
publish_tlsa_records certificate.x509
|
43
|
+
rescue Nsupdate::Error
|
44
|
+
abort "nsupdate failed." # detail logged in Nsupdate
|
45
|
+
end
|
46
|
+
|
47
|
+
def register_account
|
48
|
+
return if account_key_path.exist?
|
49
|
+
|
50
|
+
logger.debug "No key found at #{account_key_path}, registering"
|
51
|
+
registration = client.register contact: "mailto:#{@options[:contact]}"
|
52
|
+
registration.agree_terms
|
53
|
+
end
|
54
|
+
|
55
|
+
def client
|
56
|
+
@client ||= Acme::Client.new(private_key: account_key, endpoint: @options[:endpoint]).tap do |client|
|
57
|
+
client.connection.response :detailed_logger, @logger if @options[:verbose]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def account_key_path
|
62
|
+
@account_key_path ||= datadir.join ".#{@options[:contact]}.pem"
|
63
|
+
end
|
64
|
+
|
65
|
+
def datadir
|
66
|
+
@datadir ||= Pathname.new(@options[:datadir]).tap(&:mkpath)
|
67
|
+
end
|
68
|
+
|
69
|
+
def account_key
|
70
|
+
@account_key ||= read_or_create_key account_key_path
|
71
|
+
end
|
72
|
+
|
73
|
+
def read_or_create_key path
|
74
|
+
logger.debug "Creating or reading #{path}"
|
75
|
+
path.write OpenSSL::PKey::RSA.new @options[:keylength] unless path.exist?
|
76
|
+
OpenSSL::PKey::RSA.new path.read
|
77
|
+
end
|
78
|
+
|
79
|
+
def renewal_needed?
|
80
|
+
return true if @options[:force]
|
81
|
+
|
82
|
+
cert_path = live_path.join("cert.pem")
|
83
|
+
return true unless cert_path.exist?
|
84
|
+
|
85
|
+
cert = OpenSSL::X509::Certificate.new(cert_path.read)
|
86
|
+
(cert.not_after - Time.now) <= RENEWAL_THRESHOLD
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_nsupdate
|
90
|
+
Nsupdate.new(logger).tap do |nsupdate|
|
91
|
+
nsupdate.server @options[:master] if @options[:master]
|
92
|
+
nsupdate.tsig(*@options[:tsig].split(":")) if @options[:tsig]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def csr
|
97
|
+
logger.debug "Generating CSR"
|
98
|
+
Acme::Client::CertificateRequest.new(names: @options[:domains], private_key: private_key)
|
99
|
+
end
|
100
|
+
|
101
|
+
def private_key
|
102
|
+
@private_key ||= read_or_create_key private_key_path
|
103
|
+
end
|
104
|
+
|
105
|
+
def private_key_path
|
106
|
+
@private_key_path ||= live_path.join("privkey.pem")
|
107
|
+
end
|
108
|
+
|
109
|
+
def live_path
|
110
|
+
@live_path ||= datadir.join("live").join(@options[:domains].first).tap(&:mkpath)
|
111
|
+
end
|
112
|
+
|
113
|
+
def archive_path
|
114
|
+
@archive_path ||= datadir.join("archive")
|
115
|
+
.join(Time.now.strftime("%Y%m%d%H%M%S"))
|
116
|
+
.join(@options[:domains].first)
|
117
|
+
.tap(&:mkpath)
|
118
|
+
end
|
119
|
+
|
120
|
+
def write_files path, certificate, key
|
121
|
+
logger.info "Writing files to #{path}"
|
122
|
+
logger.debug "Writing #{path.join("key.pem")}"
|
123
|
+
path.join("privkey.pem").write key.to_pem
|
124
|
+
path.join("privkey.pem").chmod(0600)
|
125
|
+
logger.debug "Writing #{path.join("cert.pem")}"
|
126
|
+
path.join("cert.pem").write certificate.to_pem
|
127
|
+
logger.debug "Writing #{path.join("chain.pem")}"
|
128
|
+
path.join("chain.pem").write certificate.chain_to_pem
|
129
|
+
logger.debug "Writing #{path.join("fullchain.pem")}"
|
130
|
+
path.join("fullchain.pem").write certificate.fullchain_to_pem
|
131
|
+
end
|
132
|
+
|
133
|
+
def publish_tlsa_records certificate
|
134
|
+
return if @options[:notlsa]
|
135
|
+
|
136
|
+
logger.info "Publishing TLSA records"
|
137
|
+
old_contents = outdated_certificates.map {|certificate|
|
138
|
+
"3 1 1 #{OpenSSL::Digest::SHA256.hexdigest(certificate.public_key.to_der)}"
|
139
|
+
}.uniq
|
140
|
+
content = "3 1 1 #{OpenSSL::Digest::SHA256.hexdigest(certificate.public_key.to_der)}"
|
141
|
+
old_contents.delete(content)
|
142
|
+
|
143
|
+
@options[:domains].each do |domain|
|
144
|
+
nsupdate = build_nsupdate
|
145
|
+
|
146
|
+
@options[:tlsaports].each do |port|
|
147
|
+
restriction, port = port.split(":")
|
148
|
+
restriction, port = port, restriction unless port
|
149
|
+
label = "_#{port}._tcp.#{domain}"
|
150
|
+
|
151
|
+
if restriction
|
152
|
+
restrictions = restriction.delete("[]").split(" ")
|
153
|
+
unless restrictions.include? domain
|
154
|
+
logger.debug "Not publishing TLSA record for #{label}, not one of #{restrictions.join(" ")}"
|
155
|
+
next
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
old_contents.each do |old_content|
|
160
|
+
nsupdate.del label, "TLSA", old_content unless @options[:keep]
|
161
|
+
end
|
162
|
+
nsupdate.del label, "TLSA", content
|
163
|
+
nsupdate.add label, "TLSA", content, @options[:tlsa_ttl]
|
164
|
+
end
|
165
|
+
|
166
|
+
begin
|
167
|
+
nsupdate.send
|
168
|
+
rescue Nsupdate::Error
|
169
|
+
# Continue trying other zones, errors logged in Nsupdate
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def outdated_certificates
|
175
|
+
domain = @options[:domains].first
|
176
|
+
@outdated_certificates ||= datadir
|
177
|
+
.join("archive")
|
178
|
+
.children
|
179
|
+
.select {|dir| dir.join(domain, "cert.pem").exist? }
|
180
|
+
.sort_by(&:basename)
|
181
|
+
.map {|path| OpenSSL::X509::Certificate.new path.join(domain, "cert.pem").read }
|
182
|
+
.tap(&:pop) # keep current
|
183
|
+
.tap(&:pop) # keep previous
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
module AcmeNsupdate
|
4
|
+
class Nsupdate
|
5
|
+
class Error < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(logger)
|
9
|
+
@logger = logger
|
10
|
+
@commands = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def server server
|
14
|
+
@commands << "server #{server}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def tsig name, key
|
18
|
+
@commands << "key #{name} #{key}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def add label, type, data, ttl
|
22
|
+
@commands << "update add #{label} #{ttl} #{type} #{data}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def del label, type=nil, data=nil
|
26
|
+
@commands << "update del #{label}#{" #{type}" if type}#{" #{data}" if data}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def send
|
30
|
+
@logger.debug("Starting nsupdate:")
|
31
|
+
Open3.popen3("nsupdate") do |stdin, stdout, stderr, wait_thr|
|
32
|
+
@commands.each do |command|
|
33
|
+
@logger.debug " #{command}"
|
34
|
+
stdin.puts command
|
35
|
+
end
|
36
|
+
@logger.debug(" send")
|
37
|
+
stdin.puts "send"
|
38
|
+
stdin.close
|
39
|
+
errors = stdout.readlines.map {|line| line[/^>\s*(.*)$/, 1].strip }.reject(&:empty?)
|
40
|
+
errors.concat stderr.readlines.map(&:strip).reject(&:empty?)
|
41
|
+
stdout.close
|
42
|
+
stderr.close
|
43
|
+
unless errors.empty?
|
44
|
+
errors = errors.join(" ")
|
45
|
+
@logger.error "DNS update transaction failed: #{errors}"
|
46
|
+
@logger.info "Transaction:"
|
47
|
+
@commands.each do |command|
|
48
|
+
@logger.info " #{command}"
|
49
|
+
end
|
50
|
+
raise Error.new errors
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "acme_nsupdate/strategy"
|
2
|
+
|
3
|
+
module AcmeNsupdate
|
4
|
+
module Strategies
|
5
|
+
class Dns01
|
6
|
+
IDENTIFIER = "dns-01"
|
7
|
+
|
8
|
+
include Strategy
|
9
|
+
|
10
|
+
def initialize client
|
11
|
+
@client = client
|
12
|
+
end
|
13
|
+
|
14
|
+
def publish_challenges
|
15
|
+
@client.logger.debug "Publishing challenges for #{@client.options[:domains].join(", ")}"
|
16
|
+
|
17
|
+
challenges = @client.options[:domains].map {|domain|
|
18
|
+
nsupdate = @client.build_nsupdate
|
19
|
+
|
20
|
+
authorization = @client.client.authorize domain: domain
|
21
|
+
challenge = authorization.dns01
|
22
|
+
abort "Challenge dns-01 not supported by the given ACME server" unless challenge
|
23
|
+
nsupdate.del(*record(domain, challenge, true)) unless @client.options[:keep]
|
24
|
+
nsupdate.add(*record(domain, challenge), @client.options[:txt_ttl])
|
25
|
+
nsupdate.send
|
26
|
+
|
27
|
+
[domain, challenge]
|
28
|
+
}.to_h
|
29
|
+
|
30
|
+
@client.logger.info "Waiting 120 seconds for the DNS updates to go live"
|
31
|
+
sleep 120 # We wait some time to give the slaves time to update
|
32
|
+
|
33
|
+
challenges
|
34
|
+
end
|
35
|
+
|
36
|
+
def cleanup challenges
|
37
|
+
@client.logger.info("Cleaning up challenges")
|
38
|
+
challenges.each do |domain, challenge|
|
39
|
+
nsupdate = @client.build_nsupdate
|
40
|
+
nsupdate.del(*record(domain, challenge))
|
41
|
+
nsupdate.send
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def record domain, challenge, nodata=false
|
48
|
+
["#{challenge.record_name}.#{domain}", challenge.record_type].tap do |record|
|
49
|
+
record << %("#{challenge.record_content}") unless nodata
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
require "acme_nsupdate/strategy"
|
4
|
+
|
5
|
+
module AcmeNsupdate
|
6
|
+
module Strategies
|
7
|
+
class Http01
|
8
|
+
IDENTIFIER = "http-01"
|
9
|
+
|
10
|
+
include Strategy
|
11
|
+
|
12
|
+
def initialize client
|
13
|
+
@client = client
|
14
|
+
end
|
15
|
+
|
16
|
+
def publish_challenges
|
17
|
+
@client.logger.debug "Publishing challenges for #{@client.options[:domains].join(", ")}"
|
18
|
+
@client.options[:domains].map {|domain|
|
19
|
+
authorization = @client.client.authorize domain: domain
|
20
|
+
challenge = authorization.http01
|
21
|
+
abort "Challenge http-01 not supported by this ACME server" unless challenge
|
22
|
+
path = path challenge
|
23
|
+
@client.logger.debug "Writing #{path} for #{domain}"
|
24
|
+
FileUtils.mkdir_p File.dirname path
|
25
|
+
File.write path, challenge.file_content
|
26
|
+
[domain, challenge]
|
27
|
+
}.to_h
|
28
|
+
end
|
29
|
+
|
30
|
+
def cleanup challenges
|
31
|
+
@client.logger.info("Cleaning up challenges")
|
32
|
+
challenges.each_value do |challenge|
|
33
|
+
path = path challenge
|
34
|
+
@client.logger.debug("Removing #{path}")
|
35
|
+
File.delete path if File.exist? path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def path challenge
|
42
|
+
File.join(@client.options[:webroot], challenge.filename)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module AcmeNsupdate
|
2
|
+
module Strategy
|
3
|
+
class << self
|
4
|
+
def strategies
|
5
|
+
@strategies ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def for identifier
|
9
|
+
strategies.fetch(identifier) { raise ArgumentError.new "Unknown strategy #{identifier}!" }
|
10
|
+
end
|
11
|
+
|
12
|
+
def included base
|
13
|
+
strategies[base::IDENTIFIER] = base
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def verify_domains
|
18
|
+
@client.logger.info("Validating domains")
|
19
|
+
publish_challenges.tap do |challenges|
|
20
|
+
wait_for_verification challenges
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def wait_for_verification challenges
|
27
|
+
@client.logger.debug("Requesting verification")
|
28
|
+
challenges.each_value(&:request_verification)
|
29
|
+
@client.logger.debug("Waiting for verification")
|
30
|
+
challenges.map {|_, challenge| Thread.new { sleep(5) while challenge.verify_status == "pending" } }.each(&:join)
|
31
|
+
challenges.each do |domain, challenge|
|
32
|
+
raise "Verification of #{domain} failed: #{challenge.error}" unless challenge.status == "valid"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require "acme_nsupdate/strategies/http01.rb"
|
39
|
+
require "acme_nsupdate/strategies/dns01.rb"
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acme_nsupdate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonne Haß
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: slop
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: acme-client
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.4.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.4.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday-detailed_logger
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: |-
|
84
|
+
CLI tool to obtain certificates via ACME and update the matching TLSA records.
|
85
|
+
The primary authentication method is http-01 via webroot for now, but dns-01 is supported too.
|
86
|
+
email:
|
87
|
+
- me@jhass.eu
|
88
|
+
executables:
|
89
|
+
- acme_nsupdate
|
90
|
+
extensions: []
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- bin/acme_nsupdate
|
96
|
+
- lib/acme_nsupdate.rb
|
97
|
+
- lib/acme_nsupdate/cli.rb
|
98
|
+
- lib/acme_nsupdate/client.rb
|
99
|
+
- lib/acme_nsupdate/nsupdate.rb
|
100
|
+
- lib/acme_nsupdate/strategies/dns01.rb
|
101
|
+
- lib/acme_nsupdate/strategies/http01.rb
|
102
|
+
- lib/acme_nsupdate/strategy.rb
|
103
|
+
- lib/acme_nsupdate/version.rb
|
104
|
+
homepage: https://github.com/jhass/acme_nsupdate
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.5.1
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: ACME (Let's Encrypt) client with nsupdate (DDNS) integration.
|
128
|
+
test_files: []
|