Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly load openssh-gerenated private keys in SSHHook #16756

Merged
merged 1 commit into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions airflow/providers/ssh/hooks/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ class SSHHook(BaseHook):
:type keepalive_interval: int
"""

# key type name to paramiko PKey class
_default_pkey_mappings = {
'dsa': paramiko.DSSKey,
'ecdsa': paramiko.ECDSAKey,
'ed25519': paramiko.Ed25519Key,
'rsa': paramiko.RSAKey,
}
# List of classes to try loading private keys as, ordered (roughly) by most common to least common
_pkey_loaders = (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were never using the keys of this dict, so I changed it to just a list anyway.

paramiko.RSAKey,
paramiko.ECDSAKey,
paramiko.Ed25519Key,
paramiko.DSSKey,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix 1.

)

_host_key_mappings = {
'rsa': paramiko.RSAKey,
Expand Down Expand Up @@ -357,15 +357,17 @@ def _pkey_from_private_key(self, private_key: str, passphrase: Optional[str] = N
Creates appropriate paramiko key for given private key

:param private_key: string containing private key
:return: `paramiko.PKey` appropriate for given key
:return: ``paramiko.PKey`` appropriate for given key
:raises AirflowException: if key cannot be read
"""
allowed_pkey_types = self._default_pkey_mappings.values()
for pkey_type in allowed_pkey_types:
for pkey_class in self._pkey_loaders:
try:
key = pkey_type.from_private_key(StringIO(private_key), password=passphrase)
key = pkey_class.from_private_key(StringIO(private_key), password=passphrase)
# Test it acutally works. If Paramiko loads an openssh generated key, sometimes it will
# happily load it as the wrong type, only to fail when actually used.
key.sign_ssh_data(b'')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix 2.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

return key
except paramiko.ssh_exception.SSHException:
except (paramiko.ssh_exception.SSHException, ValueError):
continue
raise AirflowException(
'Private key provided cannot be read by paramiko.'
Expand Down
43 changes: 43 additions & 0 deletions tests/providers/ssh/hooks/test_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
import json
import random
import string
import textwrap
import unittest
from io import StringIO
from typing import Optional
from unittest import mock

import paramiko

from airflow import settings
from airflow.models import Connection
from airflow.providers.ssh.hooks.ssh import SSHHook
from airflow.utils import db
Expand Down Expand Up @@ -473,6 +475,47 @@ def test_ssh_connection_with_no_host_key_where_no_host_key_check_is_false(self,
assert ssh_client.return_value.connect.called is True
assert ssh_client.return_value.get_host_keys.return_value.add.called is False

def test_openssh_private_key(self):
# Paramiko behaves differently with OpenSSH generated keys to paramiko
# generated keys, so we need a test one.
# This has been gernerated specifically to put here, it is not otherwise in use
TEST_OPENSSH_PRIVATE_KEY = "-----BEGIN OPENSSH " + textwrap.dedent(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string is split here here is to avoid the "no private keys" precommit check.

"""\
PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAIEAuPKIGPWtIpMDrXwMAvNKQlhQ1gXV/tKyufElw/n6hrr6lvtfGhwX
DihHMsAF+8+KKWQjWgh0fttbIF3+3C56Ns8hgvgMQJT2nyWd7egwqn+LQa08uCEBEka3MO
arKzj39P66EZ/KQDD29VErlVOd97dPhaR8pOZvzcHxtLbU6rMAAAIA3uBiZd7gYmUAAAAH
c3NoLXJzYQAAAIEAuPKIGPWtIpMDrXwMAvNKQlhQ1gXV/tKyufElw/n6hrr6lvtfGhwXDi
hHMsAF+8+KKWQjWgh0fttbIF3+3C56Ns8hgvgMQJT2nyWd7egwqn+LQa08uCEBEka3MOar
Kzj39P66EZ/KQDD29VErlVOd97dPhaR8pOZvzcHxtLbU6rMAAAADAQABAAAAgA2QC5b4/T
dZ3J0uSZs1yC5RV6w6RVUokl68Zm6WuF6E+7dyu6iogrBRF9eK6WVr9M/QPh9uG0zqPSaE
fhobdm7KeycXmtDtrJnXE2ZSk4oU29++TvYZBrAqAli9aHlSArwiLnOIMzY/kIHoSJLJmd
jwXykdQ7QAd93KPEnkaMzBAAAAQGTyp6/wWqtqpMmYJ5prCGNtpVOGthW5upeiuQUytE/K
5pyPoq6dUCUxQpkprtkuNAv/ff9nW6yy1v2DWohKfaEAAABBAO3y+erRXmiMreMOAd1S84
RK2E/LUHOvClQqf6GnVavmIgkxIYEgjcFiWv4xIkTc1/FN6aX5aT4MB3srvuM7sxEAAABB
AMb6QAkvxo4hT/xKY0E0nG7zCUMXeBV35MEXQK0/InFC7aZ0tjzFsQJzLe/7q7ljIf+9/O
rCqNhxgOrv7XrRuYMAAAAKYXNoQHNpbm9wZQE=
-----END OPENSSH PRIVATE KEY-----
"""
)

session = settings.Session()
try:
conn = Connection(
conn_id='openssh_pkey',
host='localhost',
conn_type='ssh',
extra={"private_key": TEST_OPENSSH_PRIVATE_KEY},
)
session.add(conn)
session.flush()
hook = SSHHook(ssh_conn_id=conn.conn_id)
assert isinstance(hook.pkey, paramiko.RSAKey)
finally:
session.delete(conn)
session.commit()


if __name__ == '__main__':
unittest.main()