amarillo 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/amarillo +111 -0
  3. data/lib/amarillo.rb +203 -0
  4. metadata +101 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a94e70709b0f4147b7c04711d1e1f58a54eeea7a743364ee4e8dc2d25984b83a
4
+ data.tar.gz: 1f179ac5eea2d4919e22d09a44b491cbed3e2704128d9da71a9c00a10c177275
5
+ SHA512:
6
+ metadata.gz: 3acd69905f4138ab59a2de3a1cb8493a6e1ea9363ccaaa622c8d565112850e78ce11ea6fad940052c7a4795a3e19980bb2f24f392775ac4aeab463f19ef6d013
7
+ data.tar.gz: 0025b7ca2705e934750ca1bf267644ddc9fd610072ab64a2d5fc7c762b1c16d200b6ea1c6f74b87723c2a75fe6251982478ee72cfc9bc7c5b11581211b2d1a9d
data/bin/amarillo ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright 2021 iAchieved.it LLC
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 THE
21
+ # SOFTWARE.
22
+
23
+ require 'optparse'
24
+ require 'fileutils'
25
+ require 'amarillo'
26
+
27
+ options = {}
28
+ OptionParser.new do |opts|
29
+ opts.on("-z", "--zone ZONE", "Hosted zone") do |z|
30
+ options[:zone] = z
31
+ end
32
+
33
+ opts.on("-e", "--email EMAIL", "E-mail address") do |e|
34
+ options[:email] = e
35
+ end
36
+
37
+ opts.on("-n", "--name COMMONNAME", "Certificate Common Name") do |n|
38
+ options[:name] = n
39
+ end
40
+
41
+ opts.on("-o", "--output-directory OUTPUT_PATH", "Output directory of certificates and keys") do |o|
42
+ options[:certificate_path] = o
43
+ end
44
+
45
+ opts.on("-h", "--help") do |h|
46
+ options[:help] = h
47
+ end
48
+ end.parse!
49
+
50
+ if options[:help] then
51
+ manpage = <<-"HEREDOC"
52
+ Usage: amarillo --zone ZONE --name COMMONNAME --email EMAIL [--output-path OUTPUT_PATH]
53
+ HEREDOC
54
+ puts manpage
55
+ exit 0
56
+ end
57
+
58
+ if options[:zone].nil? or
59
+ options[:email].nil? or
60
+ options[:name].nil? then
61
+
62
+ puts "Usage: amarillo --zone ZONE --name COMMONNAME --email EMAIL [--output-directory OUTPUT_PATH]"
63
+
64
+ exit -1
65
+ end
66
+
67
+ if options[:output_path].nil?
68
+ certificate_path = "/etc/ssl/amarillo"
69
+ key_path = "/etc/ssl/amarillo/private"
70
+ else
71
+ certificate_path = options[:output_path]
72
+ key_path = "#{certificate_path}/private"
73
+ end
74
+
75
+ # Try to create the certificate_path and key_path
76
+ keypath_dirname = File.dirname(key_path)
77
+ unless File.directory?(keypath_dirname)
78
+ begin
79
+ FileUtils.mkdir_p(keypath_dirname)
80
+ rescue
81
+ writableMessage = <<-"HEREDOC"
82
+ Error: #{certificate_path} and #{key_path} are not writable directories to store keys and certificates.
83
+ HEREDOC
84
+ print writableMessage
85
+ exit -1
86
+ end
87
+ end
88
+
89
+ # Check for existense of aws.env
90
+ awsEnvPath = Pathname.new("/etc/amarillo/aws.env")
91
+ if not awsEnvPath.exist? then
92
+ awsEnvMessage = <<-"HEREDOC"
93
+ Error: /etc/amarillo/aws.env AWS credentials file not found.
94
+
95
+ /etc/amarillo/aws.env must exist and set both aws_access_key_id and aws_secret_access_key for an IAM user with AmazonRoute53FullAccess permissions.
96
+
97
+ Example:
98
+
99
+ [default]
100
+ aws_access_key_id = your_access_key_id
101
+ aws_secret_access_key = your_secret_access_key
102
+ HEREDOC
103
+
104
+ print awsEnvMessage
105
+
106
+ exit -1
107
+ end
108
+
109
+
110
+ y = Amarillo.new(certificate_path, key_path, awsEnvPath)
111
+ y.requestCertificate(options[:zone], options[:name], options[:email])
data/lib/amarillo.rb ADDED
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Inspired by Pete Keen's (pete@petekeen.net) post
4
+ # https://www.petekeen.net/lets-encrypt-without-certbot
5
+ #
6
+ # Copyright 2021 iAchieved.it LLC
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ # of this software and associated documentation files (the "Software"), to deal
10
+ # in the Software without restriction, including without limitation the rights
11
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the Software is
13
+ # furnished to do so, subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be included in
16
+ # all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ # SOFTWARE.
25
+
26
+ require 'logger' # Logging
27
+ require 'acme-client' # Let's Encrypt
28
+ require 'openssl' # Key Generation
29
+ require 'aws-sdk-core' # Credentials
30
+ require 'aws-sdk-route53' # Route 53
31
+ require 'resolv' # DNS Resolvers
32
+
33
+ class Amarillo
34
+
35
+ def initialize(certificatePath, keyPath, awsEnvPath)
36
+
37
+ @certificatePath = certificatePath
38
+ @keyPath = keyPath
39
+ @awsEnvPath = awsEnvPath
40
+
41
+ @logger = Logger.new(STDOUT)
42
+ @logger.level = Logger::INFO
43
+
44
+ end
45
+
46
+ def check_dns(domainName, nameservers, value)
47
+ valid = true
48
+
49
+ nameservers.each do |nameserver|
50
+ begin
51
+ records = Resolv::DNS.open(nameserver: nameserver) do |dns|
52
+ dns.getresources(
53
+ "_acme-challenge.#{domainName}",
54
+ Resolv::DNS::Resource::IN::TXT
55
+ )
56
+ end
57
+ records = records.map(&:strings).flatten
58
+ valid = value == records.first
59
+ rescue Resolv::ResolvError
60
+ return false
61
+ end
62
+ return false if !valid
63
+ end
64
+
65
+ valid
66
+ end
67
+
68
+ def requestCertificate(zone, commonName, email)
69
+
70
+ @logger.info "Generating 4096-bit RSA private key"
71
+ key = OpenSSL::PKey::RSA.new(4096)
72
+ client = Acme::Client.new(
73
+ private_key: key,
74
+ directory: 'https://acme-v02.api.letsencrypt.org/directory'
75
+ )
76
+
77
+ account = client.new_account(
78
+ contact: "mailto:#{email}",
79
+ terms_of_service_agreed: true
80
+ )
81
+
82
+ # Generate a certificate order
83
+ @logger.info "Creating certificate order request for #{commonName}"
84
+
85
+ order = client.new_order(identifiers: [commonName])
86
+ authorization = order.authorizations.first
87
+ label = "_acme-challenge.#{commonName}"
88
+ record_type = authorization.dns.record_type
89
+ challengeValue = authorization.dns.record_content
90
+
91
+ @logger.info "Challenge value for #{commonName} in #{zone} zone is #{challengeValue}"
92
+
93
+ # Update Route 53
94
+
95
+ shared_creds = Aws::SharedCredentials.new(path: "#{@awsEnvPath}")
96
+ Aws.config.update(credentials: shared_creds)
97
+
98
+ # TODO: Allow the user to set the region
99
+ route53 = Aws::Route53::Client.new(region: 'us-east-2')
100
+ hzone = route53.list_hosted_zones(max_items: 100)
101
+ .hosted_zones
102
+ .detect { |z| z.name == "#{zone}." }
103
+
104
+ change = {
105
+ action: 'UPSERT',
106
+ resource_record_set: {
107
+ name: label,
108
+ type: record_type,
109
+ ttl: 1,
110
+ resource_records: [
111
+ { value: "\"#{challengeValue}\"" }
112
+ ]
113
+ }
114
+ }
115
+
116
+ options = {
117
+ hosted_zone_id: hzone.id,
118
+ change_batch: {
119
+ changes: [change]
120
+ }
121
+ }
122
+
123
+ route53.change_resource_record_sets(options)
124
+
125
+ nameservers = []
126
+
127
+ @logger.info "Looking up nameservers for #{zone}"
128
+
129
+ Resolv::DNS.open(nameserver: '9.9.9.9') do |dns|
130
+ while nameservers.length == 0
131
+ nameservers = dns.getresources(
132
+ zone,
133
+ Resolv::DNS::Resource::IN::NS
134
+ ).map(&:name).map(&:to_s)
135
+ end
136
+ end
137
+
138
+ @logger.info "Waiting for DNS record to propogate"
139
+ while !check_dns(commonName, nameservers, challengeValue)
140
+ sleep 1
141
+ end
142
+
143
+ authorization.dns.request_validation
144
+
145
+ @logger.info "Requesting validation..."
146
+ authorization.dns.reload
147
+ while authorization.dns.status == 'pending'
148
+ sleep 2
149
+ @logger.info "DNS status: #{authorization.dns.status}"
150
+ authorization.dns.reload
151
+ end
152
+
153
+ @logger.info "Requesting certificate..."
154
+
155
+ cert_key = OpenSSL::PKey::RSA.new(4096)
156
+ csr = Acme::Client::CertificateRequest.new(
157
+ private_key: cert_key,
158
+ names: [commonName]
159
+ )
160
+
161
+ order.finalize(csr: csr)
162
+
163
+ sleep(1) while order.status == 'processing'
164
+
165
+ keyOutputPath = "#{@keyPath}/#{commonName}.key"
166
+ certOutputPath = "#{@certificatePath}/#{commonName}.crt"
167
+
168
+ @logger.info "Saving private key to #{keyOutputPath}"
169
+
170
+ File.open(keyOutputPath, "w") do |f|
171
+ f.puts cert_key.to_pem.to_s
172
+ end
173
+
174
+ @logger.info "Saving certificate to #{certOutputPath}"
175
+
176
+ File.open(certOutputPath, "w") do |f|
177
+ f.puts order.certificate
178
+ end
179
+
180
+ @logger.info "Cleaning up..."
181
+
182
+ change = {
183
+ action: 'DELETE',
184
+ resource_record_set: {
185
+ name: label,
186
+ type: record_type,
187
+ ttl: 1,
188
+ resource_records: [
189
+ { value: "\"#{challengeValue}\"" }
190
+ ]
191
+ }
192
+ }
193
+
194
+ options = {
195
+ hosted_zone_id: hzone.id,
196
+ change_batch: {
197
+ changes: [change]
198
+ }
199
+ }
200
+
201
+ route53.change_resource_record_sets(options)
202
+ end
203
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amarillo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - iAchieved.it LLC
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: acme-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-core
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk-route53
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.48'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.48'
69
+ description: A tool for managing Let's Encrypt dns-01 certificates
70
+ email: joe@iachieved.it
71
+ executables:
72
+ - amarillo
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - bin/amarillo
77
+ - lib/amarillo.rb
78
+ homepage: https://github.com/iachievedit/amarillo
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.0.3
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Amarillo
101
+ test_files: []