0

I'm using a python script to manage ssh fingerprint problems after a workstation(s) is reimaged.

I attempt to connect, and if I get a "REMOTE HOST IDENTIFICATION HAS CHANGED!" error, then script removes the old fingerprint, scans for the new one and adds it.

This all works great, until I get a message like this:

 Warning: the ECDSA host key for 'workstation-1-s' differs from the key for the IP address '192.168.1.132'
Offending key for IP in /home/me/.ssh/known_hosts:16
Matching host key in /home/me/.ssh/known_hosts:60
Are you sure you want to continue connecting (yes/no)?

The script waits for user input before continuing and removing the offending key.

How can I get the script to push through, or enter "no" so the script can continue with its fingerprint repair job?

Here's the relevant method:

def ssh_fingerprint_changed(node):
    """
    Checks if a node's ssh fingerprint has changed or an old key is found, which can occur when a node is reimaged.
    It does this by attempting to connect via ssh and inspecting stdout for an error message.
    :param node: the ip or hostname of the node
    :return: True if the node's fingerprint doesn't match the client's records. Else False.
    """
    cmd = ["ssh", "-q", ADMIN_USER + "@" + node, "exit"]
    completed = subprocess.run(cmd, stdout=subprocess.PIPE, universal_newlines=True)
    if completed.stdout.find("REMOTE HOST IDENTIFICATION HAS CHANGED!") == -1:
        print("REMOTE HOST IDENTIFICATION HAS CHANGED!")
        return True
    elif completed.stdout.find("Offending key") == -1:
        print("Offending key found.") # need to type "no" before this prints
        return True
    return False
43Tesseracts
  • 4,617
  • 8
  • 48
  • 94

1 Answers1

1

run (or legacy call) doesn't allow your controlling of the input/output of the process interactively. When you get the output, the process has already ended. So you're too late for the party.

Some would direct you to pexpect, or paramiko (which doesn't require calling ssh command).

Here's a workaround with Popen. I dropped your return logic. If you want to keep that, remember that at this point the process is still running, so you have to kill it (or wait for it to complete):

cmd = ["ssh", "-q", ADMIN_USER + "@" + node, "exit"]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
# loop on lines
for l in p.stdout:
    if b"Offending key" in l:
        print("Offending key found.")
        p.stdin.write(b"no\n")   # provide no + newline as the answer
rc = p.wait()  # wait for process to end, get return code

if you're sure that the only answer will be "no", and a given number of times, an alternative to the loop would be

out,err = p.communicate(b"no\n"*10)  # send 10 times no+linefeed

note the "b" prefix when scanning strings/writing data, as standard input/output/error are binary. Doesn't matter in python 2, but in python 3, omitting the b compares strings with bytes, and you'll never get a match.

Aside, I've done that with plink on Windows, but after a while, I got tired and rebuilt a version of plink with all security messages disabled/defaulting to the "optimistic" value. If the network is a company network behind firewalls and you're going to answer anything to get pass those prompts, better create a non-interactive tool from the start.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • looking through the `Popen` docs, it suggests using `Popen.communicate`. Would that be better? https://docs.python.org/3/library/subprocess.html#subprocess.Popen.communicate – 43Tesseracts Jan 26 '18 at 20:09
  • 1
    no because communicate doesn't allow you to filter the output line by line and decide what to reply. If you know that you're going to answer "no" a given number of times, then it's possible with comunicate. – Jean-François Fabre Jan 26 '18 at 20:11
  • why the b in `b"Offending key` for the strings? doesn't `universal_newlines=True` cause `stdout` to be strings? – 43Tesseracts Jan 26 '18 at 20:26
  • 1
    it's different. It's CR+LF / LF conversion. Output is still `bytes` if using python 3. – Jean-François Fabre Jan 26 '18 at 20:33
  • Last Q! Can make a new question if you prefer. Now I'm getting caught on the password request (if "offending key" is NOT found) but I can't `proc.terminate()` after the loop because it never reaches past the loop? – 43Tesseracts Jan 26 '18 at 20:52
  • this question can help: https://stackoverflow.com/questions/12118308/execute-ssh-with-password-authentication-via-windows-command-prompt. It seems that it says pretty much what I advised (using pexpect or plink to pass the password as a command line argument) – Jean-François Fabre Jan 26 '18 at 21:17
  • I don't want to provide the password though, I just want to kill the process (if it asks for password without finding the errors, I know the fingerprints are good). I'm only running ssh to check for errors and fixing keys. – 43Tesseracts Jan 26 '18 at 21:23
  • in that case, same technique. But maybe you have to read char by char because iterating on a line won't work (password prompt doesn't issue a newline). I've done a similar thing at work. Ping me in a few days (in a week day) I'll provide a char-by-char alternative. – Jean-François Fabre Jan 26 '18 at 21:31
  • https://stackoverflow.com/questions/48469916/ssh-via-python-subprocess-popen-terminate-if-password-requested – 43Tesseracts Jan 26 '18 at 21:37
  • I can't answer right now, but will keep on eye on that question after the weekend. If noone answered, I will try to. – Jean-François Fabre Jan 26 '18 at 21:42