icinga-cert-service 0.18.4
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 +504 -0
- data/README.md +344 -0
- data/bin/icinga2-cert-service.rb +278 -0
- data/bin/installer.sh +33 -0
- data/bin/test.rb +28 -0
- data/lib/cert-service.rb +323 -0
- data/lib/cert-service/backup.rb +42 -0
- data/lib/cert-service/certificate_handler.rb +466 -0
- data/lib/cert-service/configure_icinga.rb +1 -0
- data/lib/cert-service/download.rb +24 -0
- data/lib/cert-service/endpoint_handler.rb +98 -0
- data/lib/cert-service/executor.rb +34 -0
- data/lib/cert-service/in-memory-cache.rb +43 -0
- data/lib/cert-service/templates.rb +62 -0
- data/lib/cert-service/version.rb +5 -0
- data/lib/cert-service/zone_handler.rb +71 -0
- data/lib/logging.rb +61 -0
- data/lib/monkey_patches.rb +128 -0
- data/lib/util.rb +94 -0
- data/lib/validator.rb +38 -0
- metadata +246 -0
data/bin/installer.sh
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
DESTINATION_DIR="/usr/local/icinga2-cert-service"
|
4
|
+
SOURCE_DIR="/tmp/ruby-icinga-cert-service"
|
5
|
+
|
6
|
+
# ---------------------------------------------------------------------
|
7
|
+
|
8
|
+
echo "install icinga2-cert-service .."
|
9
|
+
|
10
|
+
[[ -d ${DESTINATION_DIR} ]] || mkdir -p ${DESTINATION_DIR}
|
11
|
+
|
12
|
+
cd ${SOURCE_DIR}
|
13
|
+
|
14
|
+
bundle update --quiet
|
15
|
+
gem uninstall --quiet io-console bundler
|
16
|
+
|
17
|
+
for i in lib bin templates assets
|
18
|
+
do
|
19
|
+
cp -a ${SOURCE_DIR}/${i} ${DESTINATION_DIR}/
|
20
|
+
done
|
21
|
+
|
22
|
+
if [[ -e /sbin/openrc-run ]]
|
23
|
+
then
|
24
|
+
cat << EOF >> /etc/conf.d/icinga2-cert-service
|
25
|
+
|
26
|
+
# Icinga2 cert service
|
27
|
+
CERT_SERVICE_BIN="/usr/local/icinga2-cert-service/bin/icinga2-cert-service.rb"
|
28
|
+
|
29
|
+
EOF
|
30
|
+
cp ${SOURCE_DIR}/init-script/openrc/icinga2-cert-service /etc/init.d/
|
31
|
+
fi
|
32
|
+
|
33
|
+
export CERT_SERVICE=${DESTINATION_DIR}
|
data/bin/test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# 05.10.2016 - Bodo Schulz
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# v2.1.0
|
7
|
+
|
8
|
+
# -----------------------------------------------------------------------------
|
9
|
+
|
10
|
+
require 'ruby_dig' if RUBY_VERSION < '2.3'
|
11
|
+
|
12
|
+
require 'sinatra/base'
|
13
|
+
require 'sinatra/basic_auth'
|
14
|
+
require 'json'
|
15
|
+
require 'yaml'
|
16
|
+
|
17
|
+
require_relative '../lib/cert-service'
|
18
|
+
require_relative '../lib/logging'
|
19
|
+
|
20
|
+
# -----------------------------------------------------------------------------
|
21
|
+
|
22
|
+
config = {
|
23
|
+
icinga_master: 'localhost'
|
24
|
+
}
|
25
|
+
|
26
|
+
ics = IcingaCertService::Client.new(config)
|
27
|
+
|
28
|
+
# -----------------------------------------------------------------------------
|
data/lib/cert-service.rb
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'socket'
|
6
|
+
require 'open3'
|
7
|
+
require 'resolv'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'rest-client'
|
10
|
+
require 'erb'
|
11
|
+
require 'time'
|
12
|
+
require 'date'
|
13
|
+
|
14
|
+
require_relative 'logging'
|
15
|
+
require_relative 'util'
|
16
|
+
require_relative 'monkey_patches'
|
17
|
+
require_relative 'validator'
|
18
|
+
require_relative 'cert-service/version'
|
19
|
+
require_relative 'cert-service/templates'
|
20
|
+
require_relative 'cert-service/executor'
|
21
|
+
require_relative 'cert-service/certificate_handler'
|
22
|
+
require_relative 'cert-service/endpoint_handler'
|
23
|
+
require_relative 'cert-service/zone_handler'
|
24
|
+
require_relative 'cert-service/in-memory-cache'
|
25
|
+
require_relative 'cert-service/backup'
|
26
|
+
require_relative 'cert-service/download'
|
27
|
+
|
28
|
+
# -----------------------------------------------------------------------------
|
29
|
+
|
30
|
+
module IcingaCertService
|
31
|
+
# Client Class to create on-the-fly a certificate to connect automaticly as satellite to an icinga2-master
|
32
|
+
#
|
33
|
+
#
|
34
|
+
class Client
|
35
|
+
|
36
|
+
include Logging
|
37
|
+
include Util::Tar
|
38
|
+
include IcingaCertService::Validator
|
39
|
+
include IcingaCertService::Templates
|
40
|
+
include IcingaCertService::Executor
|
41
|
+
include IcingaCertService::CertificateHandler
|
42
|
+
include IcingaCertService::EndpointHandler
|
43
|
+
include IcingaCertService::ZoneHandler
|
44
|
+
include IcingaCertService::InMemoryDataCache
|
45
|
+
include IcingaCertService::Backup
|
46
|
+
include IcingaCertService::Download
|
47
|
+
|
48
|
+
attr_accessor :icinga_version
|
49
|
+
|
50
|
+
# create a new instance
|
51
|
+
#
|
52
|
+
# @param [Hash, #read] settings to configure the Client
|
53
|
+
# @option params [String] :icinga_master The name (FQDN or IP) of the icinga2 master
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# IcingaCertService::Client.new( icinga_master: 'icinga2-master.example.com' )
|
57
|
+
#
|
58
|
+
def initialize( settings )
|
59
|
+
|
60
|
+
raise ArgumentError.new('only Hash are allowed') unless( settings.is_a?(Hash) )
|
61
|
+
raise ArgumentError.new('missing settings') if( settings.size.zero? )
|
62
|
+
|
63
|
+
@icinga_master = settings.dig(:icinga, :server)
|
64
|
+
@icinga_port = settings.dig(:icinga, :api, :port) || 5665
|
65
|
+
@icinga_api_user = settings.dig(:icinga, :api, :user) || 'root'
|
66
|
+
@icinga_api_password = settings.dig(:icinga, :api, :password) || 'icinga'
|
67
|
+
|
68
|
+
@base_directory = ENV.fetch('CERT_SERVICE', '/usr/local/icinga2-cert-service')
|
69
|
+
|
70
|
+
raise ArgumentError.new('missing \'icinga server\'') if( @icinga_master.nil? )
|
71
|
+
|
72
|
+
raise ArgumentError.new(format('wrong type. \'icinga api port\' must be an Integer, given \'%s\'', @icinga_port.class.to_s)) unless( @icinga_port.is_a?(Integer) )
|
73
|
+
raise ArgumentError.new(format('wrong type. \'icinga api user\' must be an String, given \'%s\'' , @icinga_api_user.class.to_s)) unless( @icinga_api_user.is_a?(String) )
|
74
|
+
raise ArgumentError.new(format('wrong type. \'icinga api password\' must be an String, given \'%s\'', @icinga_api_password.class.to_s)) unless( @icinga_api_password.is_a?(String) )
|
75
|
+
|
76
|
+
@tmp_directory = '/tmp/icinga-pki'
|
77
|
+
|
78
|
+
version = IcingaCertService::VERSION
|
79
|
+
date = '2018-08-20'
|
80
|
+
detect_version
|
81
|
+
|
82
|
+
logger.info('-----------------------------------------------------------------')
|
83
|
+
logger.info(' certificate service for Icinga2')
|
84
|
+
logger.info(format(' Version %s (%s)', version, date))
|
85
|
+
logger.info(' Copyright 2017-2018 Bodo Schulz')
|
86
|
+
logger.info(format(' Icinga2 base version %s', @icinga_version))
|
87
|
+
logger.info('-----------------------------------------------------------------')
|
88
|
+
logger.info('')
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
# detect the Icinga2 Version
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# detect_version
|
96
|
+
#
|
97
|
+
def detect_version
|
98
|
+
|
99
|
+
max_retries = 20
|
100
|
+
sleep_between_retries = 8
|
101
|
+
retried = 0
|
102
|
+
|
103
|
+
@icinga_version = 'unknown'
|
104
|
+
|
105
|
+
begin
|
106
|
+
#response = rest_client.get( headers )
|
107
|
+
response = RestClient::Request.execute(
|
108
|
+
method: :get,
|
109
|
+
url: format('https://%s:%d/v1/status/IcingaApplication', @icinga_master, @icinga_port ),
|
110
|
+
timeout: 5,
|
111
|
+
headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' },
|
112
|
+
user: @icinga_api_user,
|
113
|
+
password: @icinga_api_password,
|
114
|
+
verify_ssl: OpenSSL::SSL::VERIFY_NONE
|
115
|
+
)
|
116
|
+
|
117
|
+
response = response.body if(response.is_a?(RestClient::Response))
|
118
|
+
response = JSON.parse(response) if(response.is_a?(String))
|
119
|
+
results = response.dig('results') if(response.is_a?(Hash))
|
120
|
+
results = results.first if(results.is_a?(Array))
|
121
|
+
app_data = results.dig('status','icingaapplication','app')
|
122
|
+
version = app_data.dig('version') if(app_data.is_a?(Hash))
|
123
|
+
|
124
|
+
if(version.is_a?(String))
|
125
|
+
parts = version.match(/^r(?<v>[0-9]+\.{0}\.[0-9]+)(.*)/i)
|
126
|
+
@icinga_version = parts['v'].to_s.strip if(parts)
|
127
|
+
end
|
128
|
+
|
129
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
130
|
+
sleep( sleep_between_retries )
|
131
|
+
retry
|
132
|
+
rescue RestClient::ExceptionWithResponse => e
|
133
|
+
|
134
|
+
if( retried < max_retries )
|
135
|
+
retried += 1
|
136
|
+
logger.debug( format( 'connection refused (retry %d / %d)', retried, max_retries ) )
|
137
|
+
sleep( sleep_between_retries )
|
138
|
+
retry
|
139
|
+
else
|
140
|
+
raise format( 'Maximum retries (%d) reached. Giving up ...', max_retries )
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# function to read API Credentials from icinga2 Configuration
|
146
|
+
#
|
147
|
+
# @param [Hash, #read] params
|
148
|
+
# @option params [String] :api_user the API User, default is 'cert-service'
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# read_api_credentials( api_user: 'admin' )
|
152
|
+
#
|
153
|
+
# @return [String, #read] the configured Password or nil
|
154
|
+
#
|
155
|
+
def read_api_credentials(params = {})
|
156
|
+
|
157
|
+
api_user = params.dig(:api_user) || 'cert-service'
|
158
|
+
|
159
|
+
file_name = '/etc/icinga2/conf.d/api-users.conf'
|
160
|
+
|
161
|
+
file = File.open(file_name, 'r')
|
162
|
+
contents = file.read
|
163
|
+
password = nil
|
164
|
+
|
165
|
+
regexp_long = / # Match she-bang style C-comment
|
166
|
+
\/\* # Opening delimiter.
|
167
|
+
[^*]*\*+ # {normal*} Zero or more non-*, one or more *
|
168
|
+
(?: # Begin {(special normal*)*} construct.
|
169
|
+
[^*\/] # {special} a non-*, non-\/ following star.
|
170
|
+
[^*]*\*+ # More {normal*}
|
171
|
+
)* # Finish "Unrolling-the-Loop"
|
172
|
+
\/ # Closing delimiter.
|
173
|
+
/x
|
174
|
+
|
175
|
+
regex = /\"#{api_user}\"(.*){(.*)password(.*)=(.*)\"(?<password>.+[a-zA-Z0-9])\"(.*)}\n/m
|
176
|
+
|
177
|
+
# remove comments
|
178
|
+
result = contents.gsub(regexp_long, '')
|
179
|
+
|
180
|
+
# split our string into more parts
|
181
|
+
result = result.split('object ApiUser')
|
182
|
+
|
183
|
+
# now, iterate over all blocks and get the password
|
184
|
+
#
|
185
|
+
result.each do |block|
|
186
|
+
password = block.scan(regex)
|
187
|
+
|
188
|
+
next unless password.is_a?(Array) && password.count == 1
|
189
|
+
|
190
|
+
password = password.flatten.first
|
191
|
+
break
|
192
|
+
end
|
193
|
+
|
194
|
+
password
|
195
|
+
end
|
196
|
+
|
197
|
+
# add a host to 'api-users.conf'
|
198
|
+
#
|
199
|
+
# https://monitoring-portal.org/index.php?thread/41172-icinga2-api-mit-zertifikaten/&postID=251902#post251902
|
200
|
+
#
|
201
|
+
# @param [Hash, #read] params
|
202
|
+
# @option params [String] :host
|
203
|
+
#
|
204
|
+
# @example
|
205
|
+
# add_api_user( host: 'icinga2-satellite' )
|
206
|
+
#
|
207
|
+
# @return [Hash, #read] if config already created:
|
208
|
+
# * :status [Integer] 204
|
209
|
+
# * :message [String] Message
|
210
|
+
# @return nil if successful
|
211
|
+
#
|
212
|
+
def add_api_user(params)
|
213
|
+
|
214
|
+
host = params.dig(:host)
|
215
|
+
|
216
|
+
return { status: 500, message: 'no hostname to create an api user' } if( host.nil? )
|
217
|
+
|
218
|
+
file_name = '/etc/icinga2/conf.d/api-users.conf'
|
219
|
+
|
220
|
+
return { status: 500, message: format( 'api user not successful configured! file %s missing', file_name ) } unless( File.exist?(file_name) )
|
221
|
+
|
222
|
+
file = File.open(file_name, 'r')
|
223
|
+
contents = file.read
|
224
|
+
|
225
|
+
regexp_long = / # Match she-bang style C-comment
|
226
|
+
\/\* # Opening delimiter.
|
227
|
+
[^*]*\*+ # {normal*} Zero or more non-*, one or more *
|
228
|
+
(?: # Begin {(special normal*)*} construct.
|
229
|
+
[^*\/] # {special} a non-*, non-\/ following star.
|
230
|
+
[^*]*\*+ # More {normal*}
|
231
|
+
)* # Finish "Unrolling-the-Loop"
|
232
|
+
\/ # Closing delimiter.
|
233
|
+
/x
|
234
|
+
result = contents.gsub(regexp_long, '')
|
235
|
+
|
236
|
+
scan_api_user = result.scan(/object ApiUser(.*)"(?<zone>.+\S)"(.*){(.*)/).flatten
|
237
|
+
|
238
|
+
return { status: 200, message: format('the configuration for the api user %s already exists', host) } if( scan_api_user.include?(host) == true )
|
239
|
+
|
240
|
+
logger.debug(format('i miss an configuration for api user %s', host))
|
241
|
+
|
242
|
+
begin
|
243
|
+
|
244
|
+
result = write_template(
|
245
|
+
template: 'templates/conf.d/api_users.conf.erb',
|
246
|
+
destination_file: file_name,
|
247
|
+
environment: {
|
248
|
+
host: host
|
249
|
+
}
|
250
|
+
)
|
251
|
+
|
252
|
+
# logger.debug( result )
|
253
|
+
rescue => error
|
254
|
+
|
255
|
+
logger.debug(error)
|
256
|
+
end
|
257
|
+
|
258
|
+
return { status: 200, message: format('configuration for api user %s has been created', host) }
|
259
|
+
end
|
260
|
+
|
261
|
+
# reload the icinga2-master using the api
|
262
|
+
#
|
263
|
+
# @param [Hash, #read] params
|
264
|
+
# @option params [String] :request
|
265
|
+
# * HTTP_X_API_USER
|
266
|
+
# * HTTP_X_API_PASSWORD
|
267
|
+
#
|
268
|
+
def reload_icinga_config(params)
|
269
|
+
|
270
|
+
logger.info( 'restart icinga2 process')
|
271
|
+
|
272
|
+
api_user = params.dig(:request, 'HTTP_X_API_USER')
|
273
|
+
api_password = params.dig(:request, 'HTTP_X_API_PASSWORD')
|
274
|
+
|
275
|
+
return { status: 500, message: 'missing API Credentials - API_USER' } if( api_user.nil?)
|
276
|
+
return { status: 500, message: 'missing API Credentials - API_PASSWORD' } if( api_password.nil? )
|
277
|
+
|
278
|
+
password = read_api_credentials( api_user: api_user )
|
279
|
+
|
280
|
+
return { status: 500, message: 'wrong API Credentials' } if( password.nil? || api_password != password )
|
281
|
+
|
282
|
+
options = { user: api_user, password: api_password, verify_ssl: OpenSSL::SSL::VERIFY_NONE }
|
283
|
+
headers = { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
|
284
|
+
url = format('https://%s:5665/v1/actions/restart-process', @icinga_master )
|
285
|
+
|
286
|
+
rest_client = RestClient::Resource.new( URI.encode( url ), options )
|
287
|
+
|
288
|
+
begin
|
289
|
+
|
290
|
+
response = rest_client.post( {}.to_json, headers )
|
291
|
+
|
292
|
+
response = response.body if(response.is_a?(RestClient::Response))
|
293
|
+
response = JSON.parse(response) if(response.is_a?(String))
|
294
|
+
|
295
|
+
logger.debug(JSON.pretty_generate(response))
|
296
|
+
|
297
|
+
rescue RestClient::ExceptionWithResponse => e
|
298
|
+
|
299
|
+
logger.error("Error: restart-process has failed: '#{e}'")
|
300
|
+
logger.error(JSON.pretty_generate(params))
|
301
|
+
|
302
|
+
return { status: 500, message: e }
|
303
|
+
end
|
304
|
+
|
305
|
+
{ status: 200, message: 'service restarted' }
|
306
|
+
end
|
307
|
+
|
308
|
+
# returns the hostname of itself
|
309
|
+
#
|
310
|
+
def icinga2_server_name
|
311
|
+
Socket.gethostbyname(Socket.gethostname).first
|
312
|
+
end
|
313
|
+
|
314
|
+
# returns the IP of name
|
315
|
+
#
|
316
|
+
# @param [String, #read] name
|
317
|
+
#
|
318
|
+
def icinga2_server_ip( name = Socket.gethostname )
|
319
|
+
IPSocket.getaddress(name)
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
# Submodule for Backup
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module Backup
|
7
|
+
|
8
|
+
# creates a backup and saves it under '/var/lib/icinga2/backup'
|
9
|
+
#
|
10
|
+
# generated files are:
|
11
|
+
# - zones.conf
|
12
|
+
# - conf.d/api-users.conf
|
13
|
+
# - zones.d/*
|
14
|
+
#
|
15
|
+
def create_backup
|
16
|
+
|
17
|
+
source_directory = '/etc/icinga2'
|
18
|
+
backup_directory = '/var/lib/icinga2/backup'
|
19
|
+
|
20
|
+
FileUtils.mkpath(backup_directory) unless File.exist?(backup_directory)
|
21
|
+
|
22
|
+
white_list = %w[zones.d zones.conf conf.d/api-users.conf]
|
23
|
+
|
24
|
+
white_list.each do |p|
|
25
|
+
|
26
|
+
source_file = "#{source_directory}/#{p}"
|
27
|
+
|
28
|
+
destination_directory = File.dirname( source_file )
|
29
|
+
destination_directory.gsub!( source_directory, backup_directory )
|
30
|
+
|
31
|
+
FileUtils.mkpath(destination_directory) unless File.exist?(destination_directory)
|
32
|
+
|
33
|
+
if( File.directory?(source_file) )
|
34
|
+
FileUtils.cp_r(source_file, "#{backup_directory}/", noop: false, verbose: false )
|
35
|
+
else
|
36
|
+
FileUtils.cp_r(source_file, "#{backup_directory}/#{p}", noop: false, verbose: false )
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,466 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
# Submodule for CertificateHandler
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module CertificateHandler
|
7
|
+
|
8
|
+
# create a certificate
|
9
|
+
#
|
10
|
+
# @param [Hash, #read] params
|
11
|
+
# @option params [String] :host
|
12
|
+
# @option params [Hash] :request
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# create_certificate( host: 'icinga2-satellite', request: { 'HTTP_X_API_USER => 'admin', HTTP_X_API_PASSWORD' => 'admin' } } )
|
16
|
+
#
|
17
|
+
# @return [Hash, #read]
|
18
|
+
# * :status [Integer] 200 for successful, or 500 for an error
|
19
|
+
# * :master_name [String] the Name of the Icinga2-master (need to configure the satellite correctly)
|
20
|
+
# * :master_ip [String] the IP of the Icinga2-master (need to configure the satellite correctly)
|
21
|
+
# * :checksum [String] a created Checksum to retrive the certificat archive
|
22
|
+
# * :timestamp [Integer] a timestamp for the created archive
|
23
|
+
# * :timeout [Integer] the timeout for the created archive
|
24
|
+
# * :file_name [String] the archive Name
|
25
|
+
# * :path [String] the Path who stored the certificate archive
|
26
|
+
#
|
27
|
+
def create_certificate( params )
|
28
|
+
|
29
|
+
host = params.dig(:host)
|
30
|
+
api_user = params.dig(:request, 'HTTP_X_API_USER')
|
31
|
+
api_password = params.dig(:request, 'HTTP_X_API_PASSWORD')
|
32
|
+
remote_addr = params.dig(:request, 'REMOTE_ADDR')
|
33
|
+
|
34
|
+
return { status: 500, message: 'no hostname' } if( host.nil? )
|
35
|
+
return { status: 500, message: 'missing API Credentials - API_USER' } if( api_user.nil?)
|
36
|
+
return { status: 500, message: 'missing API Credentials - API_PASSWORD' } if( api_password.nil? )
|
37
|
+
|
38
|
+
password = read_api_credentials( api_user: api_user )
|
39
|
+
|
40
|
+
return { status: 500, message: 'wrong API Credentials' } if( password.nil? || api_password != password )
|
41
|
+
|
42
|
+
logger.info(format('got certificate request from %s', remote_addr))
|
43
|
+
|
44
|
+
if( @icinga_master.nil? )
|
45
|
+
begin
|
46
|
+
server_name = icinga2_server_name
|
47
|
+
rescue => e
|
48
|
+
logger.error(e)
|
49
|
+
server_name = @icinga_master
|
50
|
+
else
|
51
|
+
server_ip = icinga2_server_ip
|
52
|
+
end
|
53
|
+
else
|
54
|
+
server_name = @icinga_master
|
55
|
+
begin
|
56
|
+
server_ip = icinga2_server_ip(server_name)
|
57
|
+
rescue => e
|
58
|
+
logger.error(server_name)
|
59
|
+
logger.error(e)
|
60
|
+
|
61
|
+
server_ip = '127.0.0.1'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
pki_base_directory = '/etc/icinga2/pki'
|
66
|
+
pki_base_directory = '/var/lib/icinga2/certs' if( @icinga_version != '2.7' )
|
67
|
+
|
68
|
+
return { status: 500, message: 'no PKI directory found. Please configure first the Icinga2 Master!' } if( pki_base_directory.nil? )
|
69
|
+
|
70
|
+
pki_master_key = format('%s/%s.key', pki_base_directory, server_name)
|
71
|
+
pki_master_csr = format('%s/%s.csr', pki_base_directory, server_name)
|
72
|
+
pki_master_crt = format('%s/%s.crt', pki_base_directory, server_name)
|
73
|
+
pki_master_ca = format('%s/ca.crt', pki_base_directory)
|
74
|
+
|
75
|
+
return { status: 500, message: 'no PKI directory found. Please configure first the Icinga2 Master!' } unless( File.exist?(pki_base_directory) )
|
76
|
+
|
77
|
+
zone_base_directory = '/etc/icinga2/zone.d'
|
78
|
+
|
79
|
+
FileUtils.mkpath( format('%s/global-templates', zone_base_directory) )
|
80
|
+
FileUtils.mkpath( format('%s/%s', zone_base_directory, host) )
|
81
|
+
|
82
|
+
#
|
83
|
+
unless File.exist?(format('%s/global-templates/services.conf', zone_base_directory) )
|
84
|
+
|
85
|
+
if( File.exist?('/etc/icinga2/conf.d/services.conf') )
|
86
|
+
FileUtils.mv('/etc/icinga2/conf.d/services.conf', format('%s/global-templates/services.conf', zone_base_directory))
|
87
|
+
else
|
88
|
+
logger.error('missing services.conf under /etc/icinga2/conf.d')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
logger.debug(format('search PKI files for the Master \'%s\'', server_name))
|
93
|
+
|
94
|
+
if( !File.exist?(pki_master_key) || !File.exist?(pki_master_csr) || !File.exist?(pki_master_crt) )
|
95
|
+
logger.error('missing file')
|
96
|
+
logger.debug(pki_master_key)
|
97
|
+
logger.debug(pki_master_csr)
|
98
|
+
logger.debug(pki_master_crt)
|
99
|
+
|
100
|
+
return { status: 500, message: format('missing PKI for Icinga2 Master \'%s\'', server_name) }
|
101
|
+
end
|
102
|
+
|
103
|
+
tmp_host_directory = format('%s/%s', @tmp_directory, host)
|
104
|
+
# uid = File.stat('/etc/icinga2/conf.d').uid
|
105
|
+
# gid = File.stat('/etc/icinga2/conf.d').gid
|
106
|
+
|
107
|
+
FileUtils.rmdir(tmp_host_directory) if(File.exist?(tmp_host_directory))
|
108
|
+
FileUtils.mkpath(tmp_host_directory) unless File.exist?(tmp_host_directory)
|
109
|
+
FileUtils.chmod_R(0o777, @tmp_directory) if File.exist?(tmp_host_directory)
|
110
|
+
|
111
|
+
return { status: 500, message: 'can\'t create temporary directory' } unless File.exist?(tmp_host_directory)
|
112
|
+
|
113
|
+
salt = Digest::SHA256.hexdigest(host)
|
114
|
+
|
115
|
+
pki_satellite_key = format('%s/%s.key', tmp_host_directory, host)
|
116
|
+
pki_satellite_csr = format('%s/%s.csr', tmp_host_directory, host)
|
117
|
+
pki_satellite_crt = format('%s/%s.crt', tmp_host_directory, host)
|
118
|
+
pki_ticket = '%PKI_TICKET%'
|
119
|
+
|
120
|
+
commands = []
|
121
|
+
|
122
|
+
# icinga2 pki new-cert --cn $node --csr $node.csr --key $node.key
|
123
|
+
# icinga2 pki sign-csr --csr $node.csr --cert $node.crt
|
124
|
+
commands << format('icinga2 pki new-cert --cn %s --key %s --csr %s', host, pki_satellite_key, pki_satellite_csr)
|
125
|
+
commands << format('icinga2 pki sign-csr --csr %s --cert %s', pki_satellite_csr, pki_satellite_crt)
|
126
|
+
|
127
|
+
if( @icinga_version == '2.7' )
|
128
|
+
commands << format('icinga2 pki save-cert --key %s --cert %s --trustedcert %s/trusted-master.crt --host %s', pki_satellite_key, pki_satellite_crt, tmp_host_directory, server_name)
|
129
|
+
commands << format('icinga2 pki ticket --cn %s --salt %s', server_name, salt)
|
130
|
+
commands << format('icinga2 pki request --host %s --port 5665 --ticket %s --key %s --cert %s --trustedcert %s/trusted-master.crt --ca %s', server_name, pki_ticket, pki_satellite_key, pki_satellite_crt, tmp_host_directory, pki_master_ca)
|
131
|
+
end
|
132
|
+
|
133
|
+
pki_ticket = nil
|
134
|
+
next_command = nil
|
135
|
+
|
136
|
+
commands.each_with_index do |c, index|
|
137
|
+
|
138
|
+
next_command = commands[index + 1]
|
139
|
+
result = exec_command(cmd: c)
|
140
|
+
exec_code = result.dig(:code)
|
141
|
+
exec_message = result.dig(:message)
|
142
|
+
|
143
|
+
logger.debug( format( ' => %s', c ) )
|
144
|
+
logger.debug( format( ' - [%s] %s', exec_code, exec_message ) )
|
145
|
+
|
146
|
+
if( exec_code != true )
|
147
|
+
logger.error(exec_message)
|
148
|
+
logger.error(format(' command \'%s\'', c))
|
149
|
+
logger.error(format(' returned with exit-code \'%s\'', exec_code))
|
150
|
+
|
151
|
+
return { status: 500, message: format('Internal Error: \'%s\' - \'cmd %s\'', exec_message, c) }
|
152
|
+
end
|
153
|
+
|
154
|
+
if( exec_message =~ %r{/information\//} )
|
155
|
+
# logger.debug( 'no ticket' )
|
156
|
+
else
|
157
|
+
pki_ticket = exec_message.strip
|
158
|
+
next_command = next_command.gsub!('%PKI_TICKET%', pki_ticket) unless( next_command.nil? )
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
FileUtils.cp( pki_master_ca, format('%s/ca.crt', tmp_host_directory) )
|
163
|
+
|
164
|
+
# # TODO
|
165
|
+
# Build Checksum
|
166
|
+
# Dir[ sprintf( '%s/*', tmp_host_directory ) ].each do |file|
|
167
|
+
# if( File.directory?( file ) )
|
168
|
+
# next
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# Digest::SHA2.hexdigest( File.read( file ) )
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
|
175
|
+
# create TAR File
|
176
|
+
io = tar(tmp_host_directory)
|
177
|
+
# and compress
|
178
|
+
gz = gzip(io)
|
179
|
+
|
180
|
+
# write to filesystem
|
181
|
+
|
182
|
+
archive_name = format('%s/%s.tgz', @tmp_directory, host)
|
183
|
+
|
184
|
+
begin
|
185
|
+
file = File.open(archive_name, 'w')
|
186
|
+
|
187
|
+
file.binmode
|
188
|
+
file.write(gz.read)
|
189
|
+
rescue IOError => e
|
190
|
+
# some error occur, dir not writable etc.
|
191
|
+
logger.error(e)
|
192
|
+
ensure
|
193
|
+
file.close unless file.nil?
|
194
|
+
end
|
195
|
+
|
196
|
+
checksum = Digest::SHA2.hexdigest(File.read(archive_name))
|
197
|
+
timestamp = Time.now
|
198
|
+
timeout = timestamp.add_minutes(10)
|
199
|
+
|
200
|
+
logger.debug(format(' timestamp : %s', timestamp.to_datetime.strftime('%d-%m-%Y %H:%M:%S')))
|
201
|
+
logger.debug(format(' timeout : %s', timeout.to_datetime.strftime('%d-%m-%Y %H:%M:%S')))
|
202
|
+
|
203
|
+
# store datas in-memory
|
204
|
+
#
|
205
|
+
save(checksum, timestamp: timestamp, timeout: timeout, host: host)
|
206
|
+
|
207
|
+
# remove the temporary data
|
208
|
+
#
|
209
|
+
FileUtils.rm_rf(tmp_host_directory)
|
210
|
+
|
211
|
+
{
|
212
|
+
status: 200,
|
213
|
+
master_name: server_name,
|
214
|
+
master_ip: server_ip,
|
215
|
+
checksum: checksum,
|
216
|
+
timestamp: timestamp.to_i,
|
217
|
+
timeout: timeout.to_i,
|
218
|
+
file_name: format('%s.tgz', host),
|
219
|
+
path: @tmp_directory
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# check the certificate Data
|
225
|
+
#
|
226
|
+
# @param [Hash, #read] params
|
227
|
+
# @option params [String] :host
|
228
|
+
# @option params [Hash] :request
|
229
|
+
#
|
230
|
+
# @example
|
231
|
+
# check_certificate( host: 'icinga2-satellite', request: { 'HTTP_X_CHECKSUM' => '000000000000000000000000000000000000' } )
|
232
|
+
#
|
233
|
+
# @return [Hash, #read] for an error:
|
234
|
+
# * :status [Integer] 404 or 500
|
235
|
+
# * :message [String] Error Message
|
236
|
+
#
|
237
|
+
# @return [Hash, #read] for succesfuly:
|
238
|
+
# * :status [Integer] 200
|
239
|
+
# * :file_name [String] Filename
|
240
|
+
# * :path [String]
|
241
|
+
#
|
242
|
+
def check_certificate( params )
|
243
|
+
|
244
|
+
host = params.dig(:host)
|
245
|
+
checksum = params.dig(:request, 'HTTP_X_CHECKSUM')
|
246
|
+
api_user = params.dig(:request, 'HTTP_X_API_USER')
|
247
|
+
api_password = params.dig(:request, 'HTTP_X_API_PASSWORD')
|
248
|
+
remote_addr = params.dig(:request, 'REMOTE_ADDR')
|
249
|
+
|
250
|
+
return { status: 500, message: 'no valid data to get the certificate' } if( host.nil? || checksum.nil? )
|
251
|
+
|
252
|
+
file = format('%s/%s.tgz', @tmp_directory, host)
|
253
|
+
|
254
|
+
return { status: 404, message: 'file doesn\'t exits' } unless( File.exist?(file) )
|
255
|
+
|
256
|
+
in_memory_data = find_by_id(checksum)
|
257
|
+
generated_timeout = in_memory_data.dig(:timeout)
|
258
|
+
generated_timeout = File.mtime(file).add_minutes(10) if( generated_timeout.nil? )
|
259
|
+
|
260
|
+
check_timestamp = Time.now
|
261
|
+
|
262
|
+
return { status: 404, message: 'timed out. please ask for an new cert' } if( check_timestamp.to_i > generated_timeout.to_i )
|
263
|
+
|
264
|
+
# add params to create the endpoint not in zones.d
|
265
|
+
#
|
266
|
+
params[:satellite] = true
|
267
|
+
|
268
|
+
# add API User for this Endpoint
|
269
|
+
#
|
270
|
+
# add_api_user(params)
|
271
|
+
|
272
|
+
# add Endpoint (and API User)
|
273
|
+
# and create a backup of the generated files
|
274
|
+
#
|
275
|
+
add_endpoint(params)
|
276
|
+
|
277
|
+
# restart service to activate the new certificate
|
278
|
+
#
|
279
|
+
# reload_icinga_config(params)
|
280
|
+
|
281
|
+
{ status: 200, file_name: format('%s.tgz', host), path: @tmp_directory }
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
# validate the CA against a checksum
|
286
|
+
#
|
287
|
+
# @param [Hash, #read] params
|
288
|
+
# @option params [String] :checksum
|
289
|
+
#
|
290
|
+
def validate_certificate( params )
|
291
|
+
|
292
|
+
checksum = params.dig(:checksum)
|
293
|
+
|
294
|
+
return { status: 500, message: 'missing checksum' } if( checksum.nil? )
|
295
|
+
|
296
|
+
pki_base_directory = '/var/lib/icinga2/ca'
|
297
|
+
pki_master_ca = format('%s/ca.crt', pki_base_directory)
|
298
|
+
|
299
|
+
return { status: 500, message: 'no PKI directory found. Please configure first the Icinga2 Master!' } unless( File.exist?(pki_base_directory) )
|
300
|
+
|
301
|
+
if( checksum.be_a_checksum )
|
302
|
+
pki_master_ca_checksum = nil
|
303
|
+
pki_master_ca_checksum = Digest::MD5.hexdigest(File.read(pki_master_ca)) if( checksum.produced_by(:md5) )
|
304
|
+
pki_master_ca_checksum = Digest::SHA256.hexdigest(File.read(pki_master_ca)) if( checksum.produced_by(:sha256) )
|
305
|
+
pki_master_ca_checksum = Digest::SHA384.hexdigest(File.read(pki_master_ca)) if( checksum.produced_by(:sha384) )
|
306
|
+
pki_master_ca_checksum = Digest::SHA512.hexdigest(File.read(pki_master_ca)) if( checksum.produced_by(:sha512) )
|
307
|
+
|
308
|
+
return { status: 500, message: 'wrong checksum type. only md5, sha256, sha384 and sha512 is supported' } if( pki_master_ca_checksum.nil? )
|
309
|
+
return { status: 404, message: 'checksum not match.' } if( checksum != pki_master_ca_checksum )
|
310
|
+
return { status: 200 }
|
311
|
+
end
|
312
|
+
|
313
|
+
{ status: 500, message: 'checksum isn\'t a checksum' }
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
# sign a icinga2 satellite certificate with the new 2.8 pki feature
|
318
|
+
#
|
319
|
+
# @param [Hash, #read] params
|
320
|
+
# @option params [String] :host
|
321
|
+
# @option params [Hash] :request
|
322
|
+
#
|
323
|
+
# @example
|
324
|
+
# sign_certificate( host: 'icinga2-satellite', request: { 'HTTP_X_API_USER => 'admin', HTTP_X_API_PASSWORD' => 'admin' } } )
|
325
|
+
#
|
326
|
+
# @return [Hash, #read] for an error:
|
327
|
+
# * :status [Integer] 404 or 500
|
328
|
+
# * :message [String] Error Message
|
329
|
+
#
|
330
|
+
# @return [Hash, #read] for succesfuly:
|
331
|
+
# * :status [Integer] 200
|
332
|
+
# * :message [String]
|
333
|
+
# * :master_name [String]
|
334
|
+
# * :master_ip [String]
|
335
|
+
#
|
336
|
+
def sign_certificate( params )
|
337
|
+
|
338
|
+
host = params.dig(:host)
|
339
|
+
api_user = params.dig(:request, 'HTTP_X_API_USER')
|
340
|
+
api_password = params.dig(:request, 'HTTP_X_API_PASSWORD')
|
341
|
+
remote_addr = params.dig(:request, 'REMOTE_ADDR')
|
342
|
+
real_ip = params.dig(:request, 'HTTP_X_REAL_IP')
|
343
|
+
forwarded_for = params.dig(:request, 'HTTP_X_FORWARDED_FOR')
|
344
|
+
|
345
|
+
# logger.debug(params)
|
346
|
+
|
347
|
+
logger.error('no hostname') if( host.nil? )
|
348
|
+
logger.error('missing API Credentials - API_USER') if( api_user.nil? )
|
349
|
+
logger.error('missing API Credentials - API_PASSWORD') if( api_password.nil? )
|
350
|
+
|
351
|
+
return { status: 401, message: 'no hostname' } if( host.nil? )
|
352
|
+
return { status: 401, message: 'missing API Credentials - API_USER' } if( api_user.nil?)
|
353
|
+
return { status: 401, message: 'missing API Credentials - API_PASSWORD' } if( api_password.nil? )
|
354
|
+
|
355
|
+
password = read_api_credentials( api_user: api_user )
|
356
|
+
|
357
|
+
logger.error('wrong API Credentials') if( password.nil? || api_password != password )
|
358
|
+
logger.error('wrong Icinga2 Version (the master required => 2.8)') if( @icinga_version == '2.7' )
|
359
|
+
|
360
|
+
return { status: 401, message: 'wrong API Credentials' } if( password.nil? || api_password != password )
|
361
|
+
return { status: 401, message: 'wrong Icinga2 Version (the master required => 2.8)' } if( @icinga_version == '2.7' )
|
362
|
+
|
363
|
+
unless(remote_addr.nil? && real_ip.nil?)
|
364
|
+
logger.info('we running behind a proxy')
|
365
|
+
|
366
|
+
logger.debug("remote addr #{remote_addr}")
|
367
|
+
logger.debug("real ip #{real_ip}")
|
368
|
+
logger.debug("forwarded for #{forwarded_for}")
|
369
|
+
|
370
|
+
remote_addr = forwarded_for
|
371
|
+
end
|
372
|
+
|
373
|
+
unless( remote_addr.nil? )
|
374
|
+
host_short = host.split('.')
|
375
|
+
host_short = if( host_short.count > 0 )
|
376
|
+
host_short.first
|
377
|
+
else
|
378
|
+
host
|
379
|
+
end
|
380
|
+
|
381
|
+
remote_fqdn = Resolv.getnames(remote_addr).sort.last
|
382
|
+
remote_short = remote_fqdn.split('.')
|
383
|
+
remote_short = if( remote_short.count > 0 )
|
384
|
+
remote_short.first
|
385
|
+
else
|
386
|
+
remote_fqdn
|
387
|
+
end
|
388
|
+
|
389
|
+
logger.debug( "host_short #{host_short}" )
|
390
|
+
logger.debug( "remote_short #{remote_short}" )
|
391
|
+
|
392
|
+
logger.error(format('This client (%s) cannot sign the certificate for %s', remote_fqdn, host ) ) unless( host_short == remote_short )
|
393
|
+
|
394
|
+
return { status: 409, message: format('This client cannot sign the certificate for %s', host ) } unless( host_short == remote_short )
|
395
|
+
end
|
396
|
+
|
397
|
+
logger.info( format('sign certificate for %s', host) )
|
398
|
+
|
399
|
+
# /etc/icinga2 # icinga2 ca list | grep icinga2-satellite-1.matrix.lan | sort -k2
|
400
|
+
# e39c0b4bab4d0d9d5f97f0f54da875f0a60273b4fa3d3ef5d9be0d379e7a058b | Jan 10 04:27:38 2018 GMT | * | CN = icinga2-satellite-1.matrix.lan
|
401
|
+
# 5520324447b124a26107ded6d5e5b37d73e2cf2074bd2b5e9d8b860939f490df | Jan 10 04:51:38 2018 GMT | | CN = icinga2-satellite-1.matrix.lan
|
402
|
+
# 6775ea210c7559cf58093dbb151de1aaa3635950f696165eb4beca28487d193c | Jan 10 05:03:36 2018 GMT | | CN = icinga2-satellite-1.matrix.lan
|
403
|
+
|
404
|
+
commands = []
|
405
|
+
commands << format('icinga2 ca list | grep %s | sort -k2 | tail -1', host) # sort by date
|
406
|
+
|
407
|
+
commands.each_with_index do |c, index|
|
408
|
+
|
409
|
+
result = exec_command(cmd: c)
|
410
|
+
exec_code = result.dig(:code)
|
411
|
+
exec_message = result.dig(:message)
|
412
|
+
|
413
|
+
#logger.debug( "icinga2 ca list: '#{exec_message}'" )
|
414
|
+
#logger.debug( "exit code: '#{exec_code}' (#{exec_code.class})" )
|
415
|
+
|
416
|
+
return { status: 500, message: 'error to retrive the list of certificates with signing requests' } if( exec_code == false )
|
417
|
+
|
418
|
+
regex = /^(?<ticket>.+\S) \|(?<date>.*)\|(.*)\| CN = (?<cn>.+\S)$/
|
419
|
+
parts = exec_message.match(regex) if(exec_message.is_a?(String))
|
420
|
+
|
421
|
+
logger.debug( "parts: #{parts} (#{parts.class})" )
|
422
|
+
|
423
|
+
if(parts)
|
424
|
+
ticket = parts['ticket'].to_s.strip
|
425
|
+
date = parts['date'].to_s.tr('GMT','').strip
|
426
|
+
cn = parts['cn'].to_s.strip
|
427
|
+
|
428
|
+
result = exec_command(cmd: format('icinga2 ca sign %s',ticket))
|
429
|
+
exec_code = result.dig(:code)
|
430
|
+
exec_message = result.dig(:message)
|
431
|
+
message = exec_message.gsub('information/cli: ','')
|
432
|
+
|
433
|
+
#logger.debug("exec code : '#{exec_code}' (#{exec_code.class})" )
|
434
|
+
logger.debug("exec message: '#{exec_message.strip}'")
|
435
|
+
logger.debug("message : '#{message.strip}'")
|
436
|
+
|
437
|
+
# add 2hour to convert into CET (bad feeling)
|
438
|
+
date_time = DateTime.parse(date).new_offset('+02:00')
|
439
|
+
timestamp = date_time.to_time.to_i
|
440
|
+
|
441
|
+
# create the endpoint and the reference zone
|
442
|
+
# the endpoint are only after an reload available!
|
443
|
+
#
|
444
|
+
add_endpoint(params)
|
445
|
+
|
446
|
+
return {
|
447
|
+
status: 200,
|
448
|
+
message: message,
|
449
|
+
master_name: icinga2_server_name,
|
450
|
+
master_ip: icinga2_server_ip,
|
451
|
+
date: date_time.strftime("%Y-%m-%d %H:%M:%S"),
|
452
|
+
timestamp: timestamp
|
453
|
+
}
|
454
|
+
|
455
|
+
else
|
456
|
+
logger.error(format('i can\'t find a Ticket for host \'%s\'',host))
|
457
|
+
logger.error( parts )
|
458
|
+
|
459
|
+
return { status: 404, message: format('i can\'t find a Ticket for host \'%s\'',host) }
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
{ status: 204 }
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|