class MCollective::Security::Ssl
Impliments a public/private key based message validation system using SSL
public and private keys.
The design goal of the plugin is two fold:
-
give different security credentials to clients and servers to avoid a compromised server from sending new client requests.
-
create a token that uniquely identify the client - based on the filename of the public key
To setup you need to create a SSL
key pair that is shared by all nodes.
openssl genrsa -out mcserver-private.pem 1024 openssl rsa -in mcserver-private.pem -out mcserver-public.pem -outform PEM -pubout
Distribute the private and public file to /etc/mcollective/ssl on all the nodes. Distribute the public file to /etc/mcollective/ssl everywhere the client code runs.
Now you should create a key pair for every one of your clients, here we create one for user john - you could also if you are less concerned with client id create one pair and share it with all clients:
openssl genrsa -out john-private.pem 1024 openssl rsa -in john-private.pem -out john-public.pem -outform PEM -pubout
Each user has a unique userid, this is based on the name of the public key. In this example case the userid would be 'john-public'.
Store these somewhere like:
/home/john/.mc/john-private.pem /home/john/.mc/john-public.pem
Every users public key needs to be distributed to all the nodes, save the john one in a file called:
/etc/mcollective/ssl/clients/john-public.pem
If you wish to use registration or auditing that sends connections over MC to a central host you will need also put the server-public.pem in the clients directory.
You should be aware if you do add the node public key to the clients dir you will in effect be weakening your overall security. You should consider doing this only if you also set up an Authorization method that limits the requests the nodes can make.
client.cfg:
securityprovider = ssl plugin.ssl_server_public = /etc/mcollective/ssl/server-public.pem plugin.ssl_client_private = /home/john/.mc/john-private.pem plugin.ssl_client_public = /home/john/.mc/john-public.pem
If you have many clients per machine and dont want to configure the main config file with the public/private keys you can set the following environment variables:
export MCOLLECTIVE_SSL_PRIVATE=/home/john/.mc/john-private.pem export MCOLLECTIVE_SSL_PUBLIC=/home/john/.mc/john-public.pem
server.cfg:
securityprovider = ssl plugin.ssl_server_private = /etc/mcollective/ssl/server-private.pem plugin.ssl_server_public = /etc/mcollective/ssl/server-public.pem plugin.ssl_client_cert_dir = /etc/mcollective/etc/ssl/clients/ # Log but accept messages that may have been tampered with plugin.ssl.enforce_ttl = 0
Serialization can be configured to use either Marshal or YAML, data types in and out of mcollective will be preserved from client to server and reverse
You can configure YAML serialization:
plugins.ssl_serializer = yaml
else the default is Marshal. Use YAML if you wish to write a client using a language other than Ruby that doesn't support Marshal.
Validation is as default and is provided by MCollective::Security::Base
Initial code was contributed by Vladimir Vuksan and modified by R.I.Pienaar
Public Instance Methods
sets the caller id to the md5 of the public key
# File lib/mcollective/security/ssl.rb 188 def callerid 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) 192 else 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) 198 end 199 200 return id 201 end
Decodes a message by unserializing all the bits etc, it also validates it as valid using the psk etc
# File lib/mcollective/security/ssl.rb 90 def decodemsg(msg) 91 body = deserialize(msg.payload) 92 93 should_process_msg?(msg, body[:requestid]) 94 95 if validrequest?(body) 96 body[:body] = deserialize(body[:body]) 97 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") 102 103 body[:body] = body[:body][:ssl_msg] if body[:body].include?(:ssl_msg) 104 else 105 unless @config.pluginconf["ssl.enforce_ttl"] == nil 106 raise "Message %s is in an unknown or older security protocol, ignoring" % [request_description(body)] 107 end 108 end 109 end 110 111 return body 112 else 113 nil 114 end 115 end
Encodes a reply
# File lib/mcollective/security/ssl.rb 142 def encodereply(sender, msg, requestid, requestcallerid=nil) 143 serialized = serialize(msg) 144 digest = makehash(serialized) 145 146 147 req = create_reply(requestid, sender, serialized) 148 req[:hash] = digest 149 150 serialize(req) 151 end
Encodes a request msg
# File lib/mcollective/security/ssl.rb 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) 156 157 ssl_msg = {:ssl_msg => msg, 158 :ssl_ttl => ttl, 159 :ssl_msgtime => req[:msgtime]} 160 161 serialized = serialize(ssl_msg) 162 digest = makehash(serialized) 163 164 req[:hash] = digest 165 req[:body] = serialized 166 167 serialize(req) 168 end
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 hashing it.
This function compares and updates the unhashed ones based on the hashed ones. By default it enforces matching and presense by raising exceptions, if ssl.enforce_ttl is set to 0 it will only log warnings about violations
# File lib/mcollective/security/ssl.rb 123 def update_secure_property(msg, secure_property, property, description) 124 req = request_description(msg) 125 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] 129 else 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] 132 else 133 Log.warn("Request #{req} does not have a secure #{description}") unless msg[:body].include?(secure_property) 134 end 135 end 136 137 msg[property] = msg[:body][secure_property] if msg[:body].include?(secure_property) 138 msg[:body].delete(secure_property) 139 end
Checks the SSL
signature in the request body
# File lib/mcollective/security/ssl.rb 171 def validrequest?(req) 172 message = req[:body] 173 signature = req[:hash] 174 175 Log.debug("Validating request from #{req[:callerid]}") 176 177 if verify(public_key_file(req[:callerid]), signature, message.to_s) 178 @stats.validated 179 return true 180 else 181 @stats.unvalidated 182 raise(SecurityValidationFailed, "Received an invalid signature in message") 183 end 184 end