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/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 03047ab1..6c336d98 100644 --- a/confluent_server/confluent/plugins/shell/ssh.py +++ b/confluent_server/confluent/plugins/shell/ssh.py @@ -14,17 +14,50 @@ # 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.asbytes()).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.asbytes(), fingerprint, + 'pubkeys.ssh', 'newkey') + 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.asbytes(), fingerprint, + 'pubkeys.ssh', 'mismatch' + ) + + class SshShell(conapi.Console): def __init__(self, node, config, username='', password=''): @@ -33,7 +66,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 +78,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 @@ -58,7 +91,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, diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index 99d140c1..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') @@ -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) + self.fieldname, 'mismatch')