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 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: []