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.
@@ -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}
@@ -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
+ # -----------------------------------------------------------------------------
@@ -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