3 # Impliments a security system that encrypts payloads using AES and secures
4 # the AES encrypted data using RSA public/private key encryption.
6 # The design goals of this plugin are:
8 # - Each actor - clients and servers - can have their own set of public and
10 # - All actors are uniquely and cryptographically identified
11 # - Requests are encrypted using the clients private key and anyone that has
12 # the public key can see the request. Thus an atacker may see the requests
13 # given access to network or machine due to the broadcast nature of mcollective
14 # - The message time and TTL of messages are cryptographically secured making the
15 # ensuring messages can not be replayed with fake TTLs or times
16 # - Replies are encrypted using the calling clients public key. Thus no-one but
17 # the caller can view the contents of replies.
18 # - Servers can all have their own RSA keys, or share one, or reuse keys created
19 # by other PKI using software like Puppet
20 # - Requests from servers - like registration data - can be secured even to external
21 # eaves droppers depending on the level of configuration you are prepared to do
22 # - Given a network where you can ensure third parties are not able to access the
23 # middleware public key distribution can happen automatically
25 # Configuration Options:
26 # ======================
30 # # Enable this plugin
31 # securityprovider = aes_security
33 # # Use YAML as serializer
34 # plugin.aes.serializer = yaml
36 # # Send our public key with every request so servers can learn it
37 # plugin.aes.send_pubkey = 1
41 # # The clients public and private keys
42 # plugin.aes.client_private = /home/user/.mcollective.d/user-private.pem
43 # plugin.aes.client_public = /home/user/.mcollective.d/user.pem
47 # # Where to cache client keys or find manually distributed ones
48 # plugin.aes.client_cert_dir = /etc/mcollective/ssl/clients
50 # # Cache public keys promiscuously from the network
51 # plugin.aes.learn_pubkeys = 1
53 # # Log but accept messages that may have been tampered with
54 # plugin.aes.enforce_ttl = 0
56 # # The servers public and private keys
57 # plugin.aes.server_private = /etc/mcollective/ssl/server-private.pem
58 # plugin.aes.server_public = /etc/mcollective/ssl/server-public.pem
60 class Aes_security<Base
62 body = deserialize(msg.payload)
64 should_process_msg?(msg, body[:requestid])
66 # if we get a message that has a pubkey attached and we're set to learn
67 # then add it to the client_cert_dir this should only happen on servers
68 # since clients will get replies using their own pubkeys
69 if @config.pluginconf.include?("aes.learn_pubkeys") && @config.pluginconf["aes.learn_pubkeys"] == "1"
70 if body.include?(:sslpubkey)
72 certname = certname_from_callerid(body[:callerid])
74 certfile = "#{client_cert_dir}/#{certname}.pem"
75 unless File.exist?(certfile)
76 Log.debug("Caching client cert in #{certfile}")
77 File.open(certfile, "w") {|f| f.print body[:sslpubkey]}
84 cryptdata = {:key => body[:sslkey], :data => body[:body]}
86 if @initiated_by == :client
87 body[:body] = deserialize(decrypt(cryptdata, nil))
89 body[:body] = deserialize(decrypt(cryptdata, body[:callerid]))
91 # If we got a hash it's possible that this is a message with secure
92 # TTL and message time, attempt to decode that and transform into a
93 # traditional message.
95 # If it's not a hash it might be a old style message like old discovery
96 # ones that would just be a string so we allow that unaudited but only
97 # if enforce_ttl is disabled. This is primarly to allow a mixed old and
98 # new plugin infrastructure to work
99 if body[:body].is_a?(Hash)
100 update_secure_property(body, :aes_ttl, :ttl, "TTL")
101 update_secure_property(body, :aes_msgtime, :msgtime, "Message Time")
103 body[:body] = body[:body][:aes_msg] if body[:body].include?(:aes_msg)
105 unless @config.pluginconf["aes.enforce_ttl"] == "0"
106 raise "Message %s is in an unknown or older security protocol, ignoring" % [request_description(body)]
112 rescue MsgDoesNotMatchRequestID
115 rescue OpenSSL::PKey::RSAError
116 raise MsgDoesNotMatchRequestID, "Could not decrypt message using our key, possibly directed at another client"
118 rescue Exception => e
119 Log.warn("Could not decrypt message from client: #{e.class}: #{e}")
120 raise SecurityValidationFailed, "Could not decrypt message"
123 # To avoid tampering we turn the origin body into a hash and copy some of the protocol keys
124 # like :ttl and :msg_time into the hash before encrypting it.
126 # This function compares and updates the unencrypted ones based on the encrypted ones. By
127 # default it enforces matching and presense by raising exceptions, if aes.enforce_ttl is set
128 # to 0 it will only log warnings about violations
129 def update_secure_property(msg, secure_property, property, description)
130 req = request_description(msg)
132 unless @config.pluginconf["aes.enforce_ttl"] == "0"
133 raise "Request #{req} does not have a secure #{description}" unless msg[:body].include?(secure_property)
134 raise "Request #{req} #{description} does not match encrypted #{description} - possible tampering" unless msg[:body][secure_property] == msg[property]
136 if msg[:body].include?(secure_property)
137 Log.warn("Request #{req} #{description} does not match encrypted #{description} - possible tampering") unless msg[:body][secure_property] == msg[property]
139 Log.warn("Request #{req} does not have a secure #{description}") unless msg[:body].include?(secure_property)
143 msg[property] = msg[:body][secure_property] if msg[:body].include?(secure_property)
144 msg[:body].delete(secure_property)
148 def encodereply(sender, msg, requestid, requestcallerid)
149 crypted = encrypt(serialize(msg), requestcallerid)
151 req = create_reply(requestid, sender, crypted[:data])
152 req[:sslkey] = crypted[:key]
157 # Encodes a request msg
158 def encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60)
159 req = create_request(requestid, filter, nil, @initiated_by, target_agent, target_collective, ttl)
161 # embed the ttl and msgtime in the crypted data later we will use these in
162 # the decoding of a message to set the message ones from secure sources. this
163 # is to ensure messages are not tampered with to facility replay attacks etc
164 aes_msg = {:aes_msg => msg,
166 :aes_msgtime => req[:msgtime]}
168 crypted = encrypt(serialize(aes_msg), callerid)
170 req[:body] = crypted[:data]
171 req[:sslkey] = crypted[:key]
173 if @config.pluginconf.include?("aes.send_pubkey") && @config.pluginconf["aes.send_pubkey"] == "1"
174 if @initiated_by == :client
175 req[:sslpubkey] = File.read(client_public_key)
177 req[:sslpubkey] = File.read(server_public_key)
184 # Serializes a message using the configured encoder
186 serializer = @config.pluginconf["aes.serializer"] || "marshal"
188 Log.debug("Serializing using #{serializer}")
192 return YAML.dump(msg)
194 return Marshal.dump(msg)
198 # De-Serializes a message using the configured encoder
200 serializer = @config.pluginconf["aes.serializer"] || "marshal"
202 Log.debug("De-Serializing using #{serializer}")
206 return YAML.load(msg)
208 return Marshal.load(msg)
212 # sets the caller id to the md5 of the public key
214 if @initiated_by == :client
215 id = "cert=#{File.basename(client_public_key).gsub(/\.pem$/, '')}"
216 raise "Invalid callerid generated from client public key" unless valid_callerid?(id)
218 # servers need to set callerid as well, not usually needed but
219 # would be if you're doing registration or auditing or generating
220 # requests for some or other reason
221 id = "cert=#{File.basename(server_public_key).gsub(/\.pem$/, '')}"
222 raise "Invalid callerid generated from server public key" unless valid_callerid?(id)
228 def encrypt(string, certid)
229 if @initiated_by == :client
230 @ssl ||= SSL.new(client_public_key, client_private_key)
232 Log.debug("Encrypting message using private key")
233 return @ssl.encrypt_with_private(string)
235 # when the server is initating requests like for registration
236 # then the certid will be our callerid
237 if certid == callerid
238 Log.debug("Encrypting message using private key #{server_private_key}")
240 ssl = SSL.new(server_public_key, server_private_key)
241 return ssl.encrypt_with_private(string)
243 Log.debug("Encrypting message using public key for #{certid}")
245 ssl = SSL.new(public_key_path_for_client(certid))
246 return ssl.encrypt_with_public(string)
251 def decrypt(string, certid)
252 if @initiated_by == :client
253 @ssl ||= SSL.new(client_public_key, client_private_key)
255 Log.debug("Decrypting message using private key")
256 return @ssl.decrypt_with_private(string)
258 Log.debug("Decrypting message using public key for #{certid}")
260 ssl = SSL.new(public_key_path_for_client(certid))
261 return ssl.decrypt_with_public(string)
265 # On servers this will look in the aes.client_cert_dir for public
266 # keys matching the clientid, clientid is expected to be in the format
268 def public_key_path_for_client(clientid)
269 raise "Unknown callerid format in '#{clientid}'" unless clientid.match(/^cert=(.+)$/)
273 client_cert_dir + "/#{clientid}.pem"
276 # Figures out the client private key either from MCOLLECTIVE_AES_PRIVATE or the
277 # plugin.aes.client_private config option
278 def client_private_key
279 return ENV["MCOLLECTIVE_AES_PRIVATE"] if ENV.include?("MCOLLECTIVE_AES_PRIVATE")
281 raise("No plugin.aes.client_private configuration option specified") unless @config.pluginconf.include?("aes.client_private")
283 return @config.pluginconf["aes.client_private"]
286 # Figures out the client public key either from MCOLLECTIVE_AES_PUBLIC or the
287 # plugin.aes.client_public config option
288 def client_public_key
289 return ENV["MCOLLECTIVE_AES_PUBLIC"] if ENV.include?("MCOLLECTIVE_AES_PUBLIC")
291 raise("No plugin.aes.client_public configuration option specified") unless @config.pluginconf.include?("aes.client_public")
293 return @config.pluginconf["aes.client_public"]
296 # Figures out the server public key from the plugin.aes.server_public config option
297 def server_public_key
298 raise("No aes.server_public configuration option specified") unless @config.pluginconf.include?("aes.server_public")
299 return @config.pluginconf["aes.server_public"]
302 # Figures out the server private key from the plugin.aes.server_private config option
303 def server_private_key
304 raise("No plugin.aes.server_private configuration option specified") unless @config.pluginconf.include?("aes.server_private")
305 @config.pluginconf["aes.server_private"]
308 # Figures out where to get client public certs from the plugin.aes.client_cert_dir config option
310 raise("No plugin.aes.client_cert_dir configuration option specified") unless @config.pluginconf.include?("aes.client_cert_dir")
311 @config.pluginconf["aes.client_cert_dir"]
314 def request_description(msg)
315 "%s from %s@%s" % [msg[:requestid], msg[:callerid], msg[:senderid]]
318 # Takes our cert=foo callerids and return the foo bit else nil
319 def certname_from_callerid(id)
320 if id =~ /^cert=([\w\.\-]+)/
323 Log.warn("Received a callerid in an unexpected format: '#{id}', ignoring")