Class MCollective::Security::Aes_security
In: plugins/mcollective/security/aes_security.rb
Parent: Base

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
  • 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
   plugin.aes.learn_pubkeys = 1

   # 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

Methods

Public Instance methods

sets the caller id to the md5 of the public key

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 167
167:             def callerid
168:                 if @initiated_by == :client
169:                     return "cert=#{File.basename(client_public_key).gsub(/\.pem$/, '')}"
170:                 else
171:                     # servers need to set callerid as well, not usually needed but
172:                     # would be if you're doing registration or auditing or generating
173:                     # requests for some or other reason
174:                     return "cert=#{File.basename(server_public_key).gsub(/\.pem$/, '')}"
175:                 end
176:             end

Takes our cert=foo callerids and return the foo bit else nil

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 265
265:             def certname_from_callerid(id)
266:                 if id =~ /^cert=(.+)/
267:                     return $1
268:                 else
269:                     return nil
270:                 end
271:             end

Figures out where to get client public certs from the plugin.aes.client_cert_dir config option

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 259
259:             def client_cert_dir
260:                 raise("No plugin.aes.client_cert_dir configuration option specified") unless @config.pluginconf.include?("aes.client_cert_dir")
261:                 @config.pluginconf["aes.client_cert_dir"]
262:             end

Figures out the client private key either from MCOLLECTIVE_AES_PRIVATE or the plugin.aes.client_private config option

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 228
228:             def client_private_key
229:                 return ENV["MCOLLECTIVE_AES_PRIVATE"] if ENV.include?("MCOLLECTIVE_AES_PRIVATE")
230: 
231:                 raise("No plugin.aes.client_private configuration option specified") unless @config.pluginconf.include?("aes.client_private")
232: 
233:                 return @config.pluginconf["aes.client_private"]
234:             end

Figures out the client public key either from MCOLLECTIVE_AES_PUBLIC or the plugin.aes.client_public config option

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 238
238:             def client_public_key
239:                 return ENV["MCOLLECTIVE_AES_PUBLIC"] if ENV.include?("MCOLLECTIVE_AES_PUBLIC")
240: 
241:                 raise("No plugin.aes.client_public configuration option specified") unless @config.pluginconf.include?("aes.client_public")
242: 
243:                 return @config.pluginconf["aes.client_public"]
244:             end

[Source]

    # File plugins/mcollective/security/aes_security.rb, line 56
56:             def decodemsg(msg)
57:                 body = deserialize(msg.payload)
58: 
59:                 # if we get a message that has a pubkey attached and we're set to learn
60:                 # then add it to the client_cert_dir this should only happen on servers
61:                 # since clients will get replies using their own pubkeys
62:                 if @config.pluginconf.include?("aes.learn_pubkeys") && @config.pluginconf["aes.learn_pubkeys"] == "1"
63:                     if body.include?(:sslpubkey)
64:                         if client_cert_dir
65:                             certname = certname_from_callerid(body[:callerid])
66:                             if certname
67:                                 certfile = "#{client_cert_dir}/#{certname}.pem"
68:                                 unless File.exist?(certfile)
69:                                     Log.debug("Caching client cert in #{certfile}")
70:                                     File.open(certfile, "w") {|f| f.print body[:sslpubkey]}
71:                                 end
72:                             end
73:                         end
74:                     end
75:                 end
76: 
77:                 cryptdata = {:key => body[:sslkey], :data => body[:body]}
78: 
79:                 if @initiated_by == :client
80:                     body[:body] = deserialize(decrypt(cryptdata, nil))
81:                 else
82:                     body[:body] = deserialize(decrypt(cryptdata, body[:callerid]))
83:                 end
84: 
85:                 return body
86:             rescue OpenSSL::PKey::RSAError
87:                 raise MsgDoesNotMatchRequestID, "Could not decrypt message using our key, possibly directed at another client"
88: 
89:             rescue Exception => e
90:                 Log.warn("Could not decrypt message from client: #{e.class}: #{e}")
91:                 raise SecurityValidationFailed, "Could not decrypt message"
92:             end

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 201
201:             def decrypt(string, certid)
202:                 if @initiated_by == :client
203:                     @ssl ||= SSL.new(client_public_key, client_private_key)
204: 
205:                     Log.debug("Decrypting message using private key")
206:                     return @ssl.decrypt_with_private(string)
207:                 else
208:                     Log.debug("Decrypting message using public key for #{certid}")
209: 
210:                     ssl = SSL.new(public_key_path_for_client(certid))
211:                     return ssl.decrypt_with_public(string)
212:                 end
213:             end

De-Serializes a message using the configured encoder

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 153
153:             def deserialize(msg)
154:                 serializer = @config.pluginconf["aes.serializer"] || "marshal"
155: 
156:                 Log.debug("De-Serializing using #{serializer}")
157: 
158:                 case serializer
159:                     when "yaml"
160:                         return YAML.load(msg)
161:                     else
162:                         return Marshal.load(msg)
163:                 end
164:             end

Encodes a reply

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 95
 95:             def encodereply(sender, target, msg, requestid, requestcallerid)
 96:                 crypted = encrypt(serialize(msg), requestcallerid)
 97: 
 98:                 Log.debug("Encoded a reply for request #{requestid} for #{requestcallerid}")
 99: 
100:                 req = {:senderid => @config.identity,
101:                        :requestid => requestid,
102:                        :senderagent => sender,
103:                        :msgtarget => target,
104:                        :msgtime => Time.now.to_i,
105:                        :sslkey => crypted[:key],
106:                        :body => crypted[:data]}
107: 
108:                 serialize(req)
109:             end

Encodes a request msg

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 112
112:             def encoderequest(sender, target, msg, requestid, filter={})
113:                 crypted = encrypt(serialize(msg), callerid)
114: 
115:                 Log.debug("Encoding a request for '#{target}' with request id #{requestid}")
116: 
117:                 req = {:senderid => @config.identity,
118:                        :requestid => requestid,
119:                        :msgtarget => target,
120:                        :msgtime => Time.now.to_i,
121:                        :body => crypted,
122:                        :filter => filter,
123:                        :callerid => callerid,
124:                        :sslkey => crypted[:key],
125:                        :body => crypted[:data]}
126: 
127:                 if @config.pluginconf.include?("aes.send_pubkey") && @config.pluginconf["aes.send_pubkey"] == "1"
128:                     if @initiated_by == :client
129:                         req[:sslpubkey] = File.read(client_public_key)
130:                     else
131:                         req[:sslpubkey] = File.read(server_public_key)
132:                     end
133:                 end
134: 
135:                 serialize(req)
136:             end

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 178
178:             def encrypt(string, certid)
179:                 if @initiated_by == :client
180:                     @ssl ||= SSL.new(client_public_key, client_private_key)
181: 
182:                     Log.debug("Encrypting message using private key")
183:                     return @ssl.encrypt_with_private(string)
184:                 else
185:                     # when the server is initating requests like for registration
186:                     # then the certid will be our callerid
187:                     if certid == callerid
188:                         Log.debug("Encrypting message using private key #{server_private_key}")
189: 
190:                         ssl = SSL.new(server_public_key, server_private_key)
191:                         return ssl.encrypt_with_private(string)
192:                     else
193:                         Log.debug("Encrypting message using public key for #{certid}")
194: 
195:                         ssl = SSL.new(public_key_path_for_client(certid))
196:                         return ssl.encrypt_with_public(string)
197:                     end
198:                 end
199:             end

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

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 218
218:             def public_key_path_for_client(clientid)
219:                 raise "Unknown callerid format in '#{clientid}'" unless clientid.match(/^cert=(.+)$/)
220: 
221:                 clientid = $1
222: 
223:                 client_cert_dir + "/#{clientid}.pem"
224:             end

Serializes a message using the configured encoder

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 139
139:             def serialize(msg)
140:                 serializer = @config.pluginconf["aes.serializer"] || "marshal"
141: 
142:                 Log.debug("Serializing using #{serializer}")
143: 
144:                 case serializer
145:                     when "yaml"
146:                         return YAML.dump(msg)
147:                     else
148:                         return Marshal.dump(msg)
149:                 end
150:             end

Figures out the server private key from the plugin.aes.server_private config option

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 253
253:             def server_private_key
254:                 raise("No plugin.aes.server_private configuration option specified") unless @config.pluginconf.include?("aes.server_private")
255:                 @config.pluginconf["aes.server_private"]
256:             end

Figures out the server public key from the plugin.aes.server_public config option

[Source]

     # File plugins/mcollective/security/aes_security.rb, line 247
247:             def server_public_key
248:                 raise("No aes.server_public configuration option specified") unless @config.pluginconf.include?("aes.server_public")
249:                 return @config.pluginconf["aes.server_public"]
250:             end

[Validate]