amarillo 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/amarillo +111 -0
- data/lib/amarillo.rb +203 -0
- 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: []
|