From a574c69535edd67e2bc54a51d3f54e99a765faab Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 11 Feb 2016 09:13:21 -0500 Subject: [PATCH 1/4] 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 From 824253ae8c93eea203290d894362c9d2929e3e49 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 11 Feb 2016 11:35:20 -0500 Subject: [PATCH 2/4] Hook the custom keyhandler policy This actually uses the previously commited class, with one fix for the structure of the key as passed into the callback. --- confluent_server/confluent/plugins/shell/ssh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/shell/ssh.py b/confluent_server/confluent/plugins/shell/ssh.py index 4ce6042b..cb25998f 100644 --- a/confluent_server/confluent/plugins/shell/ssh.py +++ b/confluent_server/confluent/plugins/shell/ssh.py @@ -34,7 +34,7 @@ class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): self.node = node def missing_host_key(self, client, hostname, key): - fingerprint = 'sha512$' + hashlib.sha512(key).hexdigest() + fingerprint = 'sha512$' + hashlib.sha512(key.asbytes()).hexdigest() cfg = self.cfm.get_node_attributes( self.node, ('pubkeys.ssh', 'pubkeys.addpolicy')) if 'pubkeys.ssh' not in cfg[self.node]: @@ -89,7 +89,8 @@ class SshShell(conapi.Console): def logon(self): self.ssh = paramiko.SSHClient() - self.ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy()) + self.ssh.set_missing_host_key_policy( + HostKeyHandler(self.nodeconfig, self.node)) try: self.ssh.connect(self.node, username=self.username, password=self.password, allow_agent=False, From 774d592eb417ef98bd292922a44a232049d49ef0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 11 Feb 2016 12:08:18 -0500 Subject: [PATCH 3/4] Fix more usage mistakes --- confluent_server/confluent/plugins/shell/ssh.py | 8 +++++--- confluent_server/confluent/util.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/plugins/shell/ssh.py b/confluent_server/confluent/plugins/shell/ssh.py index cb25998f..4fc18116 100644 --- a/confluent_server/confluent/plugins/shell/ssh.py +++ b/confluent_server/confluent/plugins/shell/ssh.py @@ -42,7 +42,8 @@ class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): cfg[self.node]['pubkeys.addpolicy'] and cfg[self.node]['pubkeys.addpolicy']['value'] == 'manual'): raise cexc.PubkeyInvalid('New ssh key detected', - key, fingerprint, 'pubkeys.ssh') + key.asbytes(), fingerprint, + 'pubkeys.ssh') auditlog = log.Logger('audit') auditlog.log({'node': self.node, 'event': 'sshautoadd', 'fingerprint': fingerprint}) @@ -51,8 +52,9 @@ class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): return True elif cfg[self.node]['pubkeys.ssh']['value'] == fingerprint: return True - raise cexc.PubKeyInvalid( - 'Mismatched SSH host key detected', key, fingerprint, 'pubkeys.ssh' + raise cexc.PubkeyInvalid( + 'Mismatched SSH host key detected', key.asbytes(), fingerprint, + 'pubkeys.ssh' ) diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index 99d140c1..6c8f7422 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -93,6 +93,6 @@ class TLSCertVerifier(object): return True elif storedprint[self.node][self.fieldname]['value'] == fingerprint: return True - raise cexc.PubKeyInvalid( + raise cexc.PubkeyInvalid( 'Mismatched certificate detected', certificate, fingerprint, self.fieldname) From 44103b31f80a7a29555ec92dea183768792cd172 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Sun, 21 Feb 2016 14:44:31 -0500 Subject: [PATCH 4/4] Extend key error data Clients can now more consistently tell the difference between a new key and a mismatch. --- confluent_server/confluent/exceptions.py | 6 ++++-- confluent_server/confluent/plugins/shell/ssh.py | 4 ++-- confluent_server/confluent/util.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/confluent_server/confluent/exceptions.py b/confluent_server/confluent/exceptions.py index 32d573ef..9bf7b623 100644 --- a/confluent_server/confluent/exceptions.py +++ b/confluent_server/confluent/exceptions.py @@ -77,10 +77,12 @@ class PubkeyInvalid(ConfluentException): apierrorcode = 502 apierrorstr = '502 - Invalid certificate or key on target' - def __init__(self, text, certificate, fingerprint, attribname): + def __init__(self, text, certificate, fingerprint, attribname, event): super(PubkeyInvalid, self).__init__(self, text) self.fingerprint = fingerprint - bodydata = {'fingerprint': fingerprint, + bodydata = {'message': text, + 'event': event, + 'fingerprint': fingerprint, 'fingerprintfield': attribname, 'certificate': base64.b64encode(certificate)} self.errorbody = json.dumps(bodydata) diff --git a/confluent_server/confluent/plugins/shell/ssh.py b/confluent_server/confluent/plugins/shell/ssh.py index 4fc18116..6c336d98 100644 --- a/confluent_server/confluent/plugins/shell/ssh.py +++ b/confluent_server/confluent/plugins/shell/ssh.py @@ -43,7 +43,7 @@ class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): cfg[self.node]['pubkeys.addpolicy']['value'] == 'manual'): raise cexc.PubkeyInvalid('New ssh key detected', key.asbytes(), fingerprint, - 'pubkeys.ssh') + 'pubkeys.ssh', 'newkey') auditlog = log.Logger('audit') auditlog.log({'node': self.node, 'event': 'sshautoadd', 'fingerprint': fingerprint}) @@ -54,7 +54,7 @@ class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): return True raise cexc.PubkeyInvalid( 'Mismatched SSH host key detected', key.asbytes(), fingerprint, - 'pubkeys.ssh' + 'pubkeys.ssh', 'mismatch' ) diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index 6c8f7422..67d1e0d0 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -82,7 +82,7 @@ class TLSCertVerifier(object): # manually raise cexc.PubkeyInvalid('New certificate detected', certificate, fingerprint, - self.fieldname) + self.fieldname, 'newkey') # since the policy is not manual, go ahead and add new key # after logging to audit log auditlog = log.Logger('audit') @@ -95,4 +95,4 @@ class TLSCertVerifier(object): return True raise cexc.PubkeyInvalid( 'Mismatched certificate detected', certificate, fingerprint, - self.fieldname) + self.fieldname, 'mismatch')