From a574c69535edd67e2bc54a51d3f54e99a765faab Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 11 Feb 2016 09:13:21 -0500 Subject: [PATCH] Implement SSH host key management Like self-signed TLS certificates, SSH host keys warrant a similar security policy. This implementations follows the lead of the TLS management and uses the same policy name and interpretation, just storing the value in 'pubkeys.ssh' for the node rather than an extensible set of entry points (for now). --- .../confluent/config/attributes.py | 4 ++ .../confluent/plugins/shell/ssh.py | 37 +++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 3cbd5b8c..9da0b94a 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -278,4 +278,8 @@ node = { 'description': ('Fingerprint of the TLS certificate recognized as' 'belonging to the hardware manager of the server'), }, + 'pubkeys.ssh': { + 'description': ('Fingerprint of the SSH key of the OS running on the ' + 'system.'), + }, } diff --git a/confluent_server/confluent/plugins/shell/ssh.py b/confluent_server/confluent/plugins/shell/ssh.py index 03047ab1..4ce6042b 100644 --- a/confluent_server/confluent/plugins/shell/ssh.py +++ b/confluent_server/confluent/plugins/shell/ssh.py @@ -14,17 +14,48 @@ # See the License for the specific language governing permissions and # limitations under the License. -__author__ = 'jjohnson2' # This plugin provides an ssh implementation comforming to the 'console' # specification. consoleserver or shellserver would be equally likely # to use this. +import confluent.exceptions as cexc import confluent.interface.console as conapi +import confluent.log as log import eventlet +import hashlib paramiko = eventlet.import_patched('paramiko') +class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): + + def __init__(self, configmanager, node): + self.cfm = configmanager + self.node = node + + def missing_host_key(self, client, hostname, key): + fingerprint = 'sha512$' + hashlib.sha512(key).hexdigest() + cfg = self.cfm.get_node_attributes( + self.node, ('pubkeys.ssh', 'pubkeys.addpolicy')) + if 'pubkeys.ssh' not in cfg[self.node]: + if ('pubkeys.addpolicy' in cfg[self.node] and + cfg[self.node]['pubkeys.addpolicy'] and + cfg[self.node]['pubkeys.addpolicy']['value'] == 'manual'): + raise cexc.PubkeyInvalid('New ssh key detected', + key, fingerprint, 'pubkeys.ssh') + auditlog = log.Logger('audit') + auditlog.log({'node': self.node, 'event': 'sshautoadd', + 'fingerprint': fingerprint}) + self.cfm.set_node_attributes( + {self.node: {'pubkeys.ssh': fingerprint}}) + return True + elif cfg[self.node]['pubkeys.ssh']['value'] == fingerprint: + return True + raise cexc.PubKeyInvalid( + 'Mismatched SSH host key detected', key, fingerprint, 'pubkeys.ssh' + ) + + class SshShell(conapi.Console): def __init__(self, node, config, username='', password=''): @@ -33,7 +64,7 @@ class SshShell(conapi.Console): self.nodeconfig = config self.username = username self.password = password - self.inputmode = 0 # 0 = username, 1 = password... + self.inputmode = 0 # 0 = username, 1 = password... def recvdata(self): while self.connected: @@ -45,7 +76,7 @@ class SshShell(conapi.Console): def connect(self, callback): # for now, we just use the nodename as the presumptive ssh destination - #TODO(jjohnson2): use a 'nodeipget' utility function for architectures + # TODO(jjohnson2): use a 'nodeipget' utility function for architectures # that would rather not use the nodename as anything but an opaque # identifier self.datacallback = callback