6 # Impliments a public/private key based message validation system using SSL
7 # public and private keys.
9 # The design goal of the plugin is two fold:
11 # - give different security credentials to clients and servers to avoid
12 # a compromised server from sending new client requests.
13 # - create a token that uniquely identify the client - based on the filename
16 # To setup you need to create a SSL key pair that is shared by all nodes.
18 # openssl genrsa -out mcserver-private.pem 1024
19 # openssl rsa -in mcserver-private.pem -out mcserver-public.pem -outform PEM -pubout
21 # Distribute the private and public file to /etc/mcollective/ssl on all the nodes.
22 # Distribute the public file to /etc/mcollective/ssl everywhere the client code runs.
24 # Now you should create a key pair for every one of your clients, here we create one
25 # for user john - you could also if you are less concerned with client id create one
26 # pair and share it with all clients:
28 # openssl genrsa -out john-private.pem 1024
29 # openssl rsa -in john-private.pem -out john-public.pem -outform PEM -pubout
31 # Each user has a unique userid, this is based on the name of the public key.
32 # In this example case the userid would be 'john-public'.
34 # Store these somewhere like:
36 # /home/john/.mc/john-private.pem
37 # /home/john/.mc/john-public.pem
39 # Every users public key needs to be distributed to all the nodes, save the john one
42 # /etc/mcollective/ssl/clients/john-public.pem
44 # If you wish to use registration or auditing that sends connections over MC to a
45 # central host you will need also put the server-public.pem in the clients directory.
47 # You should be aware if you do add the node public key to the clients dir you will in
48 # effect be weakening your overall security. You should consider doing this only if
49 # you also set up an Authorization method that limits the requests the nodes can make.
53 # securityprovider = ssl
54 # plugin.ssl_server_public = /etc/mcollective/ssl/server-public.pem
55 # plugin.ssl_client_private = /home/john/.mc/john-private.pem
56 # plugin.ssl_client_public = /home/john/.mc/john-public.pem
58 # If you have many clients per machine and dont want to configure the main config file
59 # with the public/private keys you can set the following environment variables:
61 # export MCOLLECTIVE_SSL_PRIVATE=/home/john/.mc/john-private.pem
62 # export MCOLLECTIVE_SSL_PUBLIC=/home/john/.mc/john-public.pem
66 # securityprovider = ssl
67 # plugin.ssl_server_private = /etc/mcollective/ssl/server-private.pem
68 # plugin.ssl_server_public = /etc/mcollective/ssl/server-public.pem
69 # plugin.ssl_client_cert_dir = /etc/mcollective/etc/ssl/clients/
71 # # Log but accept messages that may have been tampered with
72 # plugin.ssl.enforce_ttl = 0
74 # Serialization can be configured to use either Marshal or YAML, data types
75 # in and out of mcollective will be preserved from client to server and reverse
77 # You can configure YAML serialization:
79 # plugins.ssl_serializer = yaml
81 # else the default is Marshal. Use YAML if you wish to write a client using
82 # a language other than Ruby that doesn't support Marshal.
84 # Validation is as default and is provided by MCollective::Security::Base
86 # Initial code was contributed by Vladimir Vuksan and modified by R.I.Pienaar
88 # Decodes a message by unserializing all the bits etc, it also validates
89 # it as valid using the psk etc
91 body = deserialize(msg.payload)
93 should_process_msg?(msg, body[:requestid])
95 if validrequest?(body)
96 body[:body] = deserialize(body[:body])
98 unless @initiated_by == :client
99 if body[:body].is_a?(Hash)
100 update_secure_property(body, :ssl_ttl, :ttl, "TTL")
101 update_secure_property(body, :ssl_msgtime, :msgtime, "Message Time")
103 body[:body] = body[:body][:ssl_msg] if body[:body].include?(:ssl_msg)
105 unless @config.pluginconf["ssl.enforce_ttl"] == nil
106 raise "Message %s is in an unknown or older security protocol, ignoring" % [request_description(body)]
117 # To avoid tampering we turn the origin body into a hash and copy some of the protocol keys
118 # like :ttl and :msg_time into the hash before hashing it.
120 # This function compares and updates the unhashed ones based on the hashed ones. By
121 # default it enforces matching and presense by raising exceptions, if ssl.enforce_ttl is set
122 # to 0 it will only log warnings about violations
123 def update_secure_property(msg, secure_property, property, description)
124 req = request_description(msg)
126 unless @config.pluginconf["ssl.enforce_ttl"] == "0"
127 raise "Request #{req} does not have a secure #{description}" unless msg[:body].include?(secure_property)
128 raise "Request #{req} #{description} does not match encrypted #{description} - possible tampering" unless msg[:body][secure_property] == msg[property]
130 if msg[:body].include?(secure_property)
131 Log.warn("Request #{req} #{description} does not match encrypted #{description} - possible tampering") unless msg[:body][secure_property] == msg[property]
133 Log.warn("Request #{req} does not have a secure #{description}") unless msg[:body].include?(secure_property)
137 msg[property] = msg[:body][secure_property] if msg[:body].include?(secure_property)
138 msg[:body].delete(secure_property)
142 def encodereply(sender, msg, requestid, requestcallerid=nil)
143 serialized = serialize(msg)
144 digest = makehash(serialized)
147 req = create_reply(requestid, sender, serialized)
153 # Encodes a request msg
154 def encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60)
155 req = create_request(requestid, filter, "", @initiated_by, target_agent, target_collective, ttl)
157 ssl_msg = {:ssl_msg => msg,
159 :ssl_msgtime => req[:msgtime]}
161 serialized = serialize(ssl_msg)
162 digest = makehash(serialized)
165 req[:body] = serialized
170 # Checks the SSL signature in the request body
171 def validrequest?(req)
173 signature = req[:hash]
175 Log.debug("Validating request from #{req[:callerid]}")
177 if verify(public_key_file(req[:callerid]), signature, message.to_s)
182 raise(SecurityValidationFailed, "Received an invalid signature in message")
187 # sets the caller id to the md5 of the public key
189 if @initiated_by == :client
190 id = "cert=#{File.basename(client_public_key).gsub(/\.pem$/, '')}"
191 raise "Invalid callerid generated from client public key" unless valid_callerid?(id)
193 # servers need to set callerid as well, not usually needed but
194 # would be if you're doing registration or auditing or generating
195 # requests for some or other reason
196 id = "cert=#{File.basename(server_public_key).gsub(/\.pem$/, '')}"
197 raise "Invalid callerid generated from server public key" unless valid_callerid?(id)
204 # Serializes a message using the configured encoder
206 serializer = @config.pluginconf["ssl_serializer"] || "marshal"
208 Log.debug("Serializing using #{serializer}")
212 return YAML.dump(msg)
214 return Marshal.dump(msg)
218 # De-Serializes a message using the configured encoder
220 serializer = @config.pluginconf["ssl_serializer"] || "marshal"
222 Log.debug("De-Serializing using #{serializer}")
226 return YAML.load(msg)
228 return Marshal.load(msg)
232 # Figures out where to get our private key
234 if ENV.include?("MCOLLECTIVE_SSL_PRIVATE")
235 return ENV["MCOLLECTIVE_SSL_PRIVATE"]
237 if @initiated_by == :node
238 return server_private_key
240 return client_private_key
245 # Figures out the public key to use
247 # If the node is asking do it based on caller id
248 # If the client is asking just get the node public key
249 def public_key_file(callerid = nil)
250 if @initiated_by == :client
251 return server_public_key
253 if callerid =~ /cert=([\w\.\-]+)/
256 if File.exist?("#{client_cert_dir}/#{cid}.pem")
257 return "#{client_cert_dir}/#{cid}.pem"
259 raise("Could not find a public key for #{cid} in #{client_cert_dir}/#{cid}.pem")
262 raise("Caller id is not in the expected format")
267 # Figures out the client private key either from MCOLLECTIVE_SSL_PRIVATE or the
268 # plugin.ssl_client_private config option
269 def client_private_key
270 return ENV["MCOLLECTIVE_SSL_PRIVATE"] if ENV.include?("MCOLLECTIVE_SSL_PRIVATE")
272 raise("No plugin.ssl_client_private configuration option specified") unless @config.pluginconf.include?("ssl_client_private")
274 return @config.pluginconf["ssl_client_private"]
277 # Figures out the client public key either from MCOLLECTIVE_SSL_PUBLIC or the
278 # plugin.ssl_client_public config option
279 def client_public_key
280 return ENV["MCOLLECTIVE_SSL_PUBLIC"] if ENV.include?("MCOLLECTIVE_SSL_PUBLIC")
282 raise("No plugin.ssl_client_public configuration option specified") unless @config.pluginconf.include?("ssl_client_public")
284 return @config.pluginconf["ssl_client_public"]
287 # Figures out the server private key from the plugin.ssl_server_private config option
288 def server_private_key
289 raise("No plugin.ssl_server_private configuration option specified") unless @config.pluginconf.include?("ssl_server_private")
290 @config.pluginconf["ssl_server_private"]
293 # Figures out the server public key from the plugin.ssl_server_public config option
294 def server_public_key
295 raise("No ssl_server_public configuration option specified") unless @config.pluginconf.include?("ssl_server_public")
296 return @config.pluginconf["ssl_server_public"]
299 # Figures out where to get client public certs from the plugin.ssl_client_cert_dir config option
301 raise("No plugin.ssl_client_cert_dir configuration option specified") unless @config.pluginconf.include?("ssl_client_cert_dir")
302 @config.pluginconf["ssl_client_cert_dir"]
305 # Retrieves the value of plugin.psk and builds a hash with it and the passed body
307 Log.debug("Creating message hash using #{private_key_file}")
309 sign(private_key_file, body.to_s)
312 # Code adapted from http://github.com/adamcooke/basicssl
314 def sign(key, string)
315 SSL.new(nil, key).sign(string, true)
318 # verifies a signature
319 def verify(key, signature, string)
320 SSL.new(key).verify_signature(signature, string, true)
323 def request_description(msg)
324 "%s from %s@%s" % [msg[:requestid], msg[:callerid], msg[:senderid]]