acme_nsupdate 0.2.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
+ 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 [![Gem Version](https://badge.fury.io/rb/acme_nsupdate.svg)](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,6 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path(File.join(__dir__, "..", "lib")))
3
+
4
+ require "acme_nsupdate/cli"
5
+
6
+ AcmeNsupdate::Cli.new(ARGV).run
@@ -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"
@@ -0,0 +1,3 @@
1
+ module AcmeNsupdate
2
+ VERSION = "0.2.0"
3
+ end
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: []