Class: MCollective::Security::Aes_security
- Defined in:
- lib/mcollective/security/aes_security.rb
Overview
Impliments a security system that encrypts payloads using AES and secures the AES encrypted data using RSA public/private key encryption.
The design goals of this plugin are:
-
Each actor - clients and servers - can have their own set of public and private keys
-
All actors are uniquely and cryptographically identified
-
Requests are encrypted using the clients private key and anyone that has the public key can see the request. Thus an atacker may see the requests given access to network or machine due to the broadcast nature of mcollective
-
The message time and TTL of messages are cryptographically secured making the ensuring messages can not be replayed with fake TTLs or times
-
Replies are encrypted using the calling clients public key. Thus no-one but the caller can view the contents of replies.
-
Servers can all have their own RSA keys, or share one, or reuse keys created by other PKI using software like Puppet
-
Requests from servers - like registration data - can be secured even to external eaves droppers depending on the level of configuration you are prepared to do
-
Given a network where you can ensure third parties are not able to access the middleware public key distribution can happen automatically
Configuration Options:
Common Options:
# Enable this plugin
securityprovider = aes_security
# Use YAML as serializer
plugin.aes.serializer = yaml
# Send our public key with every request so servers can learn it
plugin.aes.send_pubkey = 1
Clients:
# The clients public and private keys
plugin.aes.client_private = /home/user/.mcollective.d/user-private.pem
plugin.aes.client_public = /home/user/.mcollective.d/user.pem
Servers:
# Where to cache client keys or find manually distributed ones
plugin.aes.client_cert_dir = /etc/mcollective/ssl/clients
# Cache public keys promiscuously from the network (this requires either a ca_cert to be set
or insecure_learning to be enabled)
plugin.aes.learn_pubkeys = 1
# Do not check if client certificate can be verified by a CA
plugin.aes.insecure_learning = 1
# CA cert used to verify public keys when in learning mode
plugin.aes.ca_cert = /etc/mcollective/ssl/ca.cert
# Log but accept messages that may have been tampered with
plugin.aes.enforce_ttl = 0
# The servers public and private keys
plugin.aes.server_private = /etc/mcollective/ssl/server-private.pem
plugin.aes.server_public = /etc/mcollective/ssl/server-public.pem
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
-
#callerid ⇒ Object
sets the caller id to the md5 of the public key.
-
#certname_from_callerid(id) ⇒ Object
Takes our cert=foo callerids and return the foo bit else nil.
- #certname_from_certificate(cert) ⇒ Object
-
#client_cert_dir ⇒ Object
Figures out where to get client public certs from the plugin.aes.client_cert_dir config option.
-
#client_private_key ⇒ Object
Figures out the client private key either from MCOLLECTIVE_AES_PRIVATE or the plugin.aes.client_private config option.
-
#client_public_key ⇒ Object
Figures out the client public key either from MCOLLECTIVE_AES_PUBLIC or the plugin.aes.client_public config option.
- #decodemsg(msg) ⇒ Object
- #decrypt(string, certid) ⇒ Object
-
#deserialize(msg) ⇒ Object
De-Serializes a message using the configured encoder.
-
#encodereply(sender, msg, requestid, requestcallerid) ⇒ Object
Encodes a reply.
-
#encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl = 60) ⇒ Object
Encodes a request msg.
- #encrypt(string, certid) ⇒ Object
-
#public_key_path_for_client(clientid) ⇒ Object
On servers this will look in the aes.client_cert_dir for public keys matching the clientid, clientid is expected to be in the format set by callerid.
- #request_description(msg) ⇒ Object
-
#serialize(msg) ⇒ Object
Serializes a message using the configured encoder.
-
#server_private_key ⇒ Object
Figures out the server private key from the plugin.aes.server_private config option.
-
#server_public_key ⇒ Object
Figures out the server public key from the plugin.aes.server_public config option.
-
#update_secure_property(msg, secure_property, property, description) ⇒ Object
To avoid tampering we turn the origin body into a hash and copy some of the protocol keys like :ttl and :msg_time into the hash before encrypting it.
- #validate_certificate(client_cert, certid) ⇒ Object
Methods inherited from Base
#create_reply, #create_request, inherited, #initialize, #should_process_msg?, #valid_callerid?, #validate_filter?, #validrequest?
Constructor Details
This class inherits a constructor from MCollective::Security::Base
Instance Method Details
#callerid ⇒ Object
sets the caller id to the md5 of the public key
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/mcollective/security/aes_security.rb', line 239 def callerid if @initiated_by == :client key = client_public_key else key = server_public_key end # First try and create a X509 certificate object. If that is possible, # we lift the callerid from the cert begin ssl_cert = OpenSSL::X509::Certificate.new(File.read(key)) id = "cert=#{certname_from_certificate(ssl_cert)}" rescue # If the public key is not a certificate, use the file name as callerid id = "cert=#{File.basename(key).gsub(/\.pem$/, '')}" end return id end |
#certname_from_callerid(id) ⇒ Object
Takes our cert=foo callerids and return the foo bit else nil
380 381 382 383 384 385 386 |
# File 'lib/mcollective/security/aes_security.rb', line 380 def certname_from_callerid(id) if id =~ /^cert=([\w\.\-]+)/ return $1 else raise("Received a callerid in an unexpected format: '#{id}', ignoring") end end |
#certname_from_certificate(cert) ⇒ Object
388 389 390 391 392 393 394 395 |
# File 'lib/mcollective/security/aes_security.rb', line 388 def certname_from_certificate(cert) id = cert.subject if id.to_s =~ /^\/CN=([\w\.\-]+)/ return $1 else raise("Received a callerid in an unexpected format in an SSL certificate: '#{id}', ignoring") end end |
#client_cert_dir ⇒ Object
Figures out where to get client public certs from the plugin.aes.client_cert_dir config option
370 371 372 373 |
# File 'lib/mcollective/security/aes_security.rb', line 370 def client_cert_dir raise("No plugin.aes.client_cert_dir configuration option specified") unless @config.pluginconf.include?("aes.client_cert_dir") @config.pluginconf["aes.client_cert_dir"] end |
#client_private_key ⇒ Object
Figures out the client private key either from MCOLLECTIVE_AES_PRIVATE or the plugin.aes.client_private config option
339 340 341 342 343 344 345 |
# File 'lib/mcollective/security/aes_security.rb', line 339 def client_private_key return ENV["MCOLLECTIVE_AES_PRIVATE"] if ENV.include?("MCOLLECTIVE_AES_PRIVATE") raise("No plugin.aes.client_private configuration option specified") unless @config.pluginconf.include?("aes.client_private") return @config.pluginconf["aes.client_private"] end |
#client_public_key ⇒ Object
Figures out the client public key either from MCOLLECTIVE_AES_PUBLIC or the plugin.aes.client_public config option
349 350 351 352 353 354 355 |
# File 'lib/mcollective/security/aes_security.rb', line 349 def client_public_key return ENV["MCOLLECTIVE_AES_PUBLIC"] if ENV.include?("MCOLLECTIVE_AES_PUBLIC") raise("No plugin.aes.client_public configuration option specified") unless @config.pluginconf.include?("aes.client_public") return @config.pluginconf["aes.client_public"] end |
#decodemsg(msg) ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/mcollective/security/aes_security.rb', line 68 def decodemsg(msg) body = deserialize(msg.payload) should_process_msg?(msg, body[:requestid]) # if we get a message that has a pubkey attached and we're set to learn # then add it to the client_cert_dir this should only happen on servers # since clients will get replies using their own pubkeys if Util.str_to_bool(@config.pluginconf.fetch("aes.learn_pubkeys", false)) && body.include?(:sslpubkey) certname = certname_from_callerid(body[:callerid]) certfile = "#{client_cert_dir}/#{certname}.pem" if !File.exist?(certfile) if !Util.str_to_bool(@config.pluginconf.fetch("aes.insecure_learning", false)) if !@config.pluginconf.fetch("aes.ca_cert", nil) raise "Cannot verify certificate for '#{certname}'. No CA certificate specified." end if !validate_certificate(body[:sslpubkey], certname) raise "Unable to validate certificate '#{certname}' against CA" end Log.debug("Verified certificate '#{certname}' against CA") else Log.warn("Insecure key learning is not a secure method of key distribution. Do NOT use this mode in sensitive environments.") end Log.debug("Caching client cert in #{certfile}") File.open(certfile, "w") {|f| f.print body[:sslpubkey]} else Log.debug("Not caching client cert. File #{certfile} already exists.") end end cryptdata = {:key => body[:sslkey], :data => body[:body]} if @initiated_by == :client body[:body] = deserialize(decrypt(cryptdata, nil)) else certname = certname_from_callerid(body[:callerid]) certfile = "#{client_cert_dir}/#{certname}.pem" # if aes.ca_cert is set every certificate is validated before we try and use it if @config.pluginconf.fetch("aes.ca_cert", nil) && !validate_certificate(File.read(certfile), certname) raise "Unable to validate certificate '#{certname}' against CA" end body[:body] = deserialize(decrypt(cryptdata, body[:callerid])) # If we got a hash it's possible that this is a message with secure # TTL and message time, attempt to decode that and transform into a # traditional message. # # If it's not a hash it might be a old style message like old discovery # ones that would just be a string so we allow that unaudited but only # if enforce_ttl is disabled. This is primarly to allow a mixed old and # new plugin infrastructure to work if body[:body].is_a?(Hash) update_secure_property(body, :aes_ttl, :ttl, "TTL") update_secure_property(body, :aes_msgtime, :msgtime, "Message Time") body[:body] = body[:body][:aes_msg] if body[:body].include?(:aes_msg) else unless @config.pluginconf["aes.enforce_ttl"] == "0" raise "Message %s is in an unknown or older security protocol, ignoring" % [request_description(body)] end end end return body rescue MsgDoesNotMatchRequestID raise rescue OpenSSL::PKey::RSAError raise MsgDoesNotMatchRequestID, "Could not decrypt message using our key, possibly directed at another client" rescue Exception => e Log.warn("Could not decrypt message from client: #{e.class}: #{e}") raise SecurityValidationFailed, "Could not decrypt message" end |
#decrypt(string, certid) ⇒ Object
282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/mcollective/security/aes_security.rb', line 282 def decrypt(string, certid) if @initiated_by == :client @ssl ||= SSL.new(client_public_key, client_private_key) Log.debug("Decrypting message using private key") return @ssl.decrypt_with_private(string) else Log.debug("Decrypting message using public key for #{certid}") ssl = SSL.new(public_key_path_for_client(certid)) return ssl.decrypt_with_public(string) end end |
#deserialize(msg) ⇒ Object
De-Serializes a message using the configured encoder
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/mcollective/security/aes_security.rb', line 221 def deserialize(msg) serializer = @config.pluginconf["aes.serializer"] || "marshal" Log.debug("De-Serializing using #{serializer}") case serializer when "yaml" if YAML.respond_to? :safe_load return YAML.safe_load(msg, [Symbol, Regexp]) else raise "YAML.safe_load not supported by Ruby #{RUBY_VERSION}. Please update to Ruby 2.1+." end else return Marshal.load(msg) end end |
#encodereply(sender, msg, requestid, requestcallerid) ⇒ Object
Encodes a reply
170 171 172 173 174 175 176 177 |
# File 'lib/mcollective/security/aes_security.rb', line 170 def encodereply(sender, msg, requestid, requestcallerid) crypted = encrypt(serialize(msg), requestcallerid) req = create_reply(requestid, sender, crypted[:data]) req[:sslkey] = crypted[:key] serialize(req) end |
#encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl = 60) ⇒ Object
Encodes a request msg
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/mcollective/security/aes_security.rb', line 180 def encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60) req = create_request(requestid, filter, nil, @initiated_by, target_agent, target_collective, ttl) # embed the ttl and msgtime in the crypted data later we will use these in # the decoding of a message to set the message ones from secure sources. this # is to ensure messages are not tampered with to facility replay attacks etc aes_msg = {:aes_msg => msg, :aes_ttl => ttl, :aes_msgtime => req[:msgtime]} crypted = encrypt(serialize(aes_msg), callerid) req[:body] = crypted[:data] req[:sslkey] = crypted[:key] if @config.pluginconf.include?("aes.send_pubkey") && @config.pluginconf["aes.send_pubkey"] == "1" if @initiated_by == :client req[:sslpubkey] = File.read(client_public_key) else req[:sslpubkey] = File.read(server_public_key) end end serialize(req) end |
#encrypt(string, certid) ⇒ Object
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/mcollective/security/aes_security.rb', line 259 def encrypt(string, certid) if @initiated_by == :client @ssl ||= SSL.new(client_public_key, client_private_key) Log.debug("Encrypting message using private key") return @ssl.encrypt_with_private(string) else # when the server is initating requests like for registration # then the certid will be our callerid if certid == callerid Log.debug("Encrypting message using private key #{server_private_key}") ssl = SSL.new(server_public_key, server_private_key) return ssl.encrypt_with_private(string) else Log.debug("Encrypting message using public key for #{certid}") ssl = SSL.new(public_key_path_for_client(certid)) return ssl.encrypt_with_public(string) end end end |
#public_key_path_for_client(clientid) ⇒ Object
On servers this will look in the aes.client_cert_dir for public keys matching the clientid, clientid is expected to be in the format set by callerid
329 330 331 332 333 334 335 |
# File 'lib/mcollective/security/aes_security.rb', line 329 def public_key_path_for_client(clientid) raise "Unknown callerid format in '#{clientid}'" unless clientid.match(/^cert=(.+)$/) clientid = $1 client_cert_dir + "/#{clientid}.pem" end |
#request_description(msg) ⇒ Object
375 376 377 |
# File 'lib/mcollective/security/aes_security.rb', line 375 def request_description(msg) "%s from %s@%s" % [msg[:requestid], msg[:callerid], msg[:senderid]] end |
#serialize(msg) ⇒ Object
Serializes a message using the configured encoder
207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/mcollective/security/aes_security.rb', line 207 def serialize(msg) serializer = @config.pluginconf["aes.serializer"] || "marshal" Log.debug("Serializing using #{serializer}") case serializer when "yaml" return YAML.dump(msg) else return Marshal.dump(msg) end end |
#server_private_key ⇒ Object
Figures out the server private key from the plugin.aes.server_private config option
364 365 366 367 |
# File 'lib/mcollective/security/aes_security.rb', line 364 def server_private_key raise("No plugin.aes.server_private configuration option specified") unless @config.pluginconf.include?("aes.server_private") @config.pluginconf["aes.server_private"] end |
#server_public_key ⇒ Object
Figures out the server public key from the plugin.aes.server_public config option
358 359 360 361 |
# File 'lib/mcollective/security/aes_security.rb', line 358 def server_public_key raise("No aes.server_public configuration option specified") unless @config.pluginconf.include?("aes.server_public") return @config.pluginconf["aes.server_public"] end |
#update_secure_property(msg, secure_property, property, description) ⇒ Object
To avoid tampering we turn the origin body into a hash and copy some of the protocol keys like :ttl and :msg_time into the hash before encrypting it.
This function compares and updates the unencrypted ones based on the encrypted ones. By default it enforces matching and presense by raising exceptions, if aes.enforce_ttl is set to 0 it will only log warnings about violations
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/mcollective/security/aes_security.rb', line 151 def update_secure_property(msg, secure_property, property, description) req = request_description(msg) unless @config.pluginconf["aes.enforce_ttl"] == "0" raise "Request #{req} does not have a secure #{description}" unless msg[:body].include?(secure_property) raise "Request #{req} #{description} does not match encrypted #{description} - possible tampering" unless msg[:body][secure_property] == msg[property] else if msg[:body].include?(secure_property) Log.warn("Request #{req} #{description} does not match encrypted #{description} - possible tampering") unless msg[:body][secure_property] == msg[property] else Log.warn("Request #{req} does not have a secure #{description}") unless msg[:body].include?(secure_property) end end msg[property] = msg[:body][secure_property] if msg[:body].include?(secure_property) msg[:body].delete(secure_property) end |
#validate_certificate(client_cert, certid) ⇒ Object
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/mcollective/security/aes_security.rb', line 295 def validate_certificate(client_cert, certid) cert_file = @config.pluginconf.fetch("aes.ca_cert", nil) begin ssl_cert = OpenSSL::X509::Certificate.new(client_cert) rescue OpenSSL::X509::CertificateError Log.warn("Received public key that is not a X509 certficate") return false end ssl_certname = certname_from_certificate(ssl_cert) if certid != ssl_certname Log.warn("certname '#{certid}' doesn't match certificate '#{ssl_certname}'") return false end Log.debug("Loading CA Cert for verification") ca_cert = OpenSSL::X509::Store.new ca_cert.add_file cert_file if ca_cert.verify(ssl_cert) Log.debug("Verified certificate '#{ssl_certname}' against CA") else # TODO add cert id Log.warn("Unable to validate certificate '#{ssl_certname}'' against CA") return false end return true end |