class MCollective::Security::Ssl - mcollective version 2.12.5

# 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

callerid() click to toggle source

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

decodemsg(msg) click to toggle source

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

encodereply(sender, msg, requestid, requestcallerid=nil) click to toggle source

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

encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60) click to toggle source

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

update_secure_property(msg, secure_property, property, description) click to toggle source

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

validrequest?(req) click to toggle source

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