From 310a0f88e54796793886f8a47f6a7146c93fa92f Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 27 May 2020 16:03:05 -0400 Subject: [PATCH] remove the usage of create_temporary_fifo from credential plugins this resolves an issue that causes an endless hang on with Cyberark AIM lookups when a certificate *and* key are specified the underlying issue here is that we can't rely on the underyling Python ssl implementation to *only* read from the fifo that stores the pem data *only once*; in reality, we need to just use *actual* tempfiles for stability purposes see: https://github.com/ansible/awx/issues/6986 see: https://github.com/urllib3/urllib3/issues/1880 --- awx/main/credential_plugins/aim.py | 29 +++++----------- awx/main/credential_plugins/conjur.py | 27 ++++++--------- awx/main/credential_plugins/hashivault.py | 20 +++++------ awx/main/credential_plugins/plugin.py | 42 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 49 deletions(-) diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index c63181ed46..5853f8305f 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -1,15 +1,10 @@ -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles from urllib.parse import quote, urlencode, urljoin from django.utils.translation import ugettext_lazy as _ import requests -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - aim_inputs = { 'fields': [{ 'id': 'url', @@ -81,22 +76,14 @@ def aim_backend(**kwargs): request_qs = '?' + urlencode(query_params, quote_via=quote) request_url = urljoin(url, '/'.join(['AIMWebService', 'api', 'Accounts'])) - cert = None - if client_cert and client_key: - cert = ( - create_temporary_fifo(client_cert.encode()), - create_temporary_fifo(client_key.encode()) + with CertFiles(client_cert, client_key) as cert: + res = requests.get( + request_url + request_qs, + timeout=30, + cert=cert, + verify=verify, + allow_redirects=False, ) - elif client_cert: - cert = create_temporary_fifo(client_cert.encode()) - - res = requests.get( - request_url + request_qs, - timeout=30, - cert=cert, - verify=verify, - allow_redirects=False, - ) res.raise_for_status() return res.json()['Content'] diff --git a/awx/main/credential_plugins/conjur.py b/awx/main/credential_plugins/conjur.py index a851277134..286600dd1d 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx/main/credential_plugins/conjur.py @@ -1,4 +1,4 @@ -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles import base64 from urllib.parse import urljoin, quote_plus @@ -6,11 +6,6 @@ from urllib.parse import urljoin, quote_plus from django.utils.translation import ugettext_lazy as _ import requests -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - conjur_inputs = { 'fields': [{ @@ -66,14 +61,14 @@ def conjur_backend(**kwargs): 'data': api_key, 'allow_redirects': False, } - if cacert: - auth_kwargs['verify'] = create_temporary_fifo(cacert.encode()) - # https://www.conjur.org/api.html#authentication-authenticate-post - resp = requests.post( - urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), - **auth_kwargs - ) + with CertFiles(cacert) as cert: + # https://www.conjur.org/api.html#authentication-authenticate-post + auth_kwargs['verify'] = cert + resp = requests.post( + urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), + **auth_kwargs + ) resp.raise_for_status() token = base64.b64encode(resp.content).decode('utf-8') @@ -81,8 +76,6 @@ def conjur_backend(**kwargs): 'headers': {'Authorization': 'Token token="{}"'.format(token)}, 'allow_redirects': False, } - if cacert: - lookup_kwargs['verify'] = create_temporary_fifo(cacert.encode()) # https://www.conjur.org/api.html#secrets-retrieve-a-secret-get path = urljoin(url, '/'.join([ @@ -94,7 +87,9 @@ def conjur_backend(**kwargs): if version: path = '?'.join([path, version]) - resp = requests.get(path, timeout=30, **lookup_kwargs) + with CertFiles(cacert) as cert: + lookup_kwargs['verify'] = cert + resp = requests.get(path, timeout=30, **lookup_kwargs) resp.raise_for_status() return resp.text diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index c094f747d9..47ee73e6ad 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -3,16 +3,11 @@ import os import pathlib from urllib.parse import urljoin -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles import requests from django.utils.translation import ugettext_lazy as _ -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - base_inputs = { 'fields': [{ 'id': 'url', @@ -101,8 +96,6 @@ def kv_backend(**kwargs): 'timeout': 30, 'allow_redirects': False, } - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) sess = requests.Session() sess.headers['Authorization'] = 'Bearer {}'.format(token) @@ -129,7 +122,9 @@ def kv_backend(**kwargs): path_segments = [secret_path] request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/') - response = sess.get(request_url, **request_kwargs) + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + response = sess.get(request_url, **request_kwargs) response.raise_for_status() json = response.json() @@ -157,8 +152,6 @@ def ssh_backend(**kwargs): 'timeout': 30, 'allow_redirects': False, } - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) request_kwargs['json'] = {'public_key': kwargs['public_key']} if kwargs.get('valid_principals'): @@ -170,7 +163,10 @@ def ssh_backend(**kwargs): sess.headers['X-Vault-Token'] = token # https://www.vaultproject.io/api/secret/ssh/index.html#sign-ssh-key request_url = '/'.join([url, secret_path, 'sign', role]).rstrip('/') - resp = sess.post(request_url, **request_kwargs) + + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + resp = sess.post(request_url, **request_kwargs) resp.raise_for_status() return resp.json()['data']['signed_key'] diff --git a/awx/main/credential_plugins/plugin.py b/awx/main/credential_plugins/plugin.py index c5edde7bc1..def2676a02 100644 --- a/awx/main/credential_plugins/plugin.py +++ b/awx/main/credential_plugins/plugin.py @@ -1,3 +1,45 @@ +import os +import tempfile + from collections import namedtuple CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend']) + + +class CertFiles(): + """ + A context manager used for writing a certificate and (optional) key + to $TMPDIR, and cleaning up afterwards. + + This is particularly useful as a shared resource for credential plugins + that want to pull cert/key data out of the database and persist it + temporarily to the file system so that it can loaded into the openssl + certificate chain (generally, for HTTPS requests plugins make via the + Python requests library) + + with CertFiles(cert_data, key_data) as cert: + # cert is string representing a path to the cert or pemfile + # temporarily written to disk + requests.post(..., cert=cert) + """ + + certfile = None + + def __init__(self, cert, key=None): + self.cert = cert + self.key = key + + def __enter__(self): + if not self.cert: + return None + self.certfile = tempfile.NamedTemporaryFile('wb', delete=False) + self.certfile.write(self.cert.encode()) + if self.key: + self.certfile.write(b'\n') + self.certfile.write(self.key.encode()) + self.certfile.flush() + return str(self.certfile.name) + + def __exit__(self, *args): + if self.certfile and os.path.exists(self.certfile.name): + os.remove(self.certfile.name)