Fixes the changing of the root password so it is no longer logged (#48774)

This commit is contained in:
Tim Rupp 2018-11-15 20:32:39 -08:00 committed by GitHub
parent 2cd4224fb3
commit 04520361ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -196,7 +196,13 @@ shell:
sample: tmsh sample: tmsh
''' '''
import re import os
import tempfile
try:
from BytesIO import BytesIO
except ImportError:
from io import BytesIO
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback from ansible.module_utils.basic import env_fallback
@ -212,6 +218,7 @@ try:
from library.module_utils.network.f5.common import exit_json from library.module_utils.network.f5.common import exit_json
from library.module_utils.network.f5.common import fail_json from library.module_utils.network.f5.common import fail_json
from library.module_utils.network.f5.icontrol import tmos_version from library.module_utils.network.f5.icontrol import tmos_version
from library.module_utils.network.f5.icontrol import upload_file
except ImportError: except ImportError:
from ansible.module_utils.network.f5.bigip import F5RestClient from ansible.module_utils.network.f5.bigip import F5RestClient
from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import F5ModuleError
@ -221,6 +228,36 @@ except ImportError:
from ansible.module_utils.network.f5.common import exit_json from ansible.module_utils.network.f5.common import exit_json
from ansible.module_utils.network.f5.common import fail_json from ansible.module_utils.network.f5.common import fail_json
from ansible.module_utils.network.f5.icontrol import tmos_version from ansible.module_utils.network.f5.icontrol import tmos_version
from ansible.module_utils.network.f5.icontrol import upload_file
try:
# Crypto is used specifically for changing the root password via
# tmsh over REST.
#
# We utilize the crypto library to encrypt the contents of a file
# before we upload it, and then decrypt it on-box to change the
# password.
#
# To accomplish such a process, we need to be able to encrypt the
# temporary file with the public key found on the box.
#
# These libraries are used to do the encryption.
#
# Note that, if these are not available, the ability to change the
# root password is disabled and the user will be notified as such
# by a failure of the module.
#
# These libraries *should* be available on most Ansible controllers
# by default though as crypto is a dependency of Ansible.
#
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
class Parameters(AnsibleF5Parameters): class Parameters(AnsibleF5Parameters):
@ -297,6 +334,14 @@ class Parameters(AnsibleF5Parameters):
result.append(value) result.append(value)
return result return result
@property
def temp_upload_file(self):
if self._values['temp_upload_file'] is None:
f = tempfile.NamedTemporaryFile()
name = os.path.basename(f.name)
self._values['temp_upload_file'] = name
return self._values['temp_upload_file']
class ApiParameters(Parameters): class ApiParameters(Parameters):
@property @property
@ -784,6 +829,12 @@ class PartitionedManager(BaseManager):
class RootUserManager(BaseManager): class RootUserManager(BaseManager):
def exec_module(self): def exec_module(self):
if not HAS_CRYPTO:
raise F5ModuleError(
"An installed and up-to-date python 'cryptography' package is "
"required to change the 'root' password."
)
changed = False changed = False
result = dict() result = dict()
state = self.want.state state = self.want.state
@ -806,15 +857,126 @@ class RootUserManager(BaseManager):
return True return True
def update(self): def update(self):
public_key = self.get_public_key_from_device()
public_key = self.extract_key(public_key)
encrypted = self.encrypt_password_change_file(
public_key, self.want.password_credential
)
self.upload_to_device(encrypted, self.want.temp_upload_file)
result = self.update_on_device() result = self.update_on_device()
self.remove_uploaded_file_from_device(self.want.temp_upload_file)
return result
def encrypt_password_change_file(self, public_key, password):
# This function call requires that the public_key be expressed in bytes
pub = serialization.load_pem_public_key(
bytes(public_key, 'utf-8'),
backend=default_backend()
)
message = bytes("{0}\n{0}\n".format(password), 'utf-8')
ciphertext = pub.encrypt(
message,
# OpenSSL craziness
#
# Using this padding because it is the only one that works with
# the OpenSSL on BIG-IP at this time.
padding.PKCS1v15(),
#
# OAEP is the recommended padding to use for encrypting, however, two
# things are wrong with it on BIG-IP.
#
# The first is that one of the parameters required to decrypt the data
# is not supported by the OpenSSL version on BIG-IP. A "parameter setting"
# error is raised when you attempt to use the OAEP parameters to specify
# hashing algorithms.
#
# This is validated by this thread here
#
# https://mta.openssl.org/pipermail/openssl-dev/2017-September/009745.html
#
# Were is supported, we could use OAEP, but the second problem is that OAEP
# is not the default mode of the ``openssl`` command. Therefore, we need
# to adjust the command we use to decrypt the encrypted file when it is
# placed on BIG-IP.
#
# The correct (and recommended if BIG-IP ever upgrades OpenSSL) code is
# shown below.
#
# padding.OAEP(
# mgf=padding.MGF1(algorithm=hashes.SHA256()),
# algorithm=hashes.SHA256(),
# label=None
# )
#
# Additionally, the code in ``update_on_device()`` would need to be changed
# to pass the correct command line arguments to decrypt the file.
)
return BytesIO(ciphertext)
def extract_key(self, content):
"""Extracts the public key from the openssl command output over REST
The REST output includes some extra output that is not relevant to the
public key. This function attempts to only return the valid public key
data from the openssl output
Args:
content: The output from the REST API command to view the public key.
Returns:
string: The discovered public key
"""
lines = content.split("\n")
start = lines.index('-----BEGIN PUBLIC KEY-----')
end = lines.index('-----END PUBLIC KEY-----')
result = "\n".join(lines[start:end + 1])
return result return result
def update_on_device(self): def update_on_device(self):
escape_patterns = r'([$' + "'])"
errors = ['Bad password', 'password change canceled', 'based on a dictionary word'] errors = ['Bad password', 'password change canceled', 'based on a dictionary word']
content = "{0}\n{0}\n".format(self.want.password_credential)
command = re.sub(escape_patterns, r'\\\1', content) # Decrypting logic
cmd = '-c "printf \\\"{0}\\\" | tmsh modify auth password root"'.format(command) #
# The following commented out command will **not** work on BIG-IP versions
# utilizing OpenSSL 1.0.11-fips (15 Jan 2015).
#
# The reason is because that version of OpenSSL does not support the various
# ``-pkeyopt`` parameters shown below.
#
# Nevertheless, I am including it here as a possible future enhancement in
# case the method currently in use stops working.
#
# This command overrides defaults provided by OpenSSL because I am not
# sure how long the defaults will remain the defaults. Probably as long
# as it took OpenSSL to reach 1.0...
#
# openssl = [
# 'openssl', 'pkeyutl', '-in', '/var/config/rest/downloads/{0}'.format(self.want.temp_upload_file),
# '-decrypt', '-inkey', '/config/ssl/ssl.key/default.key',
# '-pkeyopt', 'rsa_padding_mode:oaep', '-pkeyopt', 'rsa_oaep_md:sha256',
# '-pkeyopt', 'rsa_mgf1_md:sha256'
# ]
#
# The command we actually use is (while not recommended) also the only one
# that works. It forgoes the usage of OAEP and uses the defaults that come
# with OpenSSL (PKCS1v15)
#
# See this link for information on the parameters used
#
# https://www.openssl.org/docs/manmaster/man1/pkeyutl.html
#
# If you change the command below, you will need to additionally change
# how the encryption is done in ``encrypt_password_change_file()``.
#
openssl = [
'openssl', 'pkeyutl', '-in', '/var/config/rest/downloads/{0}'.format(self.want.temp_upload_file),
'-decrypt', '-inkey', '/config/ssl/ssl.key/default.key',
]
cmd = '-c "{0} | tmsh modify auth password root"'.format(' '.join(openssl))
params = dict( params = dict(
command='run', command='run',
@ -839,6 +1001,74 @@ class RootUserManager(BaseManager):
raise F5ModuleError(resp.content) raise F5ModuleError(resp.content)
return True return True
def upload_to_device(self, content, name):
"""Uploads a file-like object via the REST API to a given filename
Args:
content: The file-like object whose content to upload
name: The remote name of the file to store the content in. The
final location of the file will be in /var/config/rest/downloads.
Returns:
void
"""
url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format(
self.client.provider['server'],
self.client.provider['server_port']
)
try:
upload_file(self.client, url, content, name)
except F5ModuleError:
raise F5ModuleError(
"Failed to upload the file."
)
def remove_uploaded_file_from_device(self, name):
filepath = '/var/config/rest/downloads/{0}'.format(name)
params = {
"command": "run",
"utilCmdArgs": filepath
}
uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def get_public_key_from_device(self):
cmd = '-c "openssl rsa -in /config/ssl/ssl.key/default.key -pubout"'
params = dict(
command='run',
utilCmdArgs=cmd
)
uri = "https://{0}:{1}/mgmt/tm/util/bash".format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
if 'commandResult' in response:
return response['commandResult']
return None
class ArgumentSpec(object): class ArgumentSpec(object):
def __init__(self): def __init__(self):