Fixes the changing of the root password so it is no longer logged (#48774)
This commit is contained in:
parent
2cd4224fb3
commit
04520361ac
1 changed files with 235 additions and 5 deletions
|
@ -196,7 +196,13 @@ shell:
|
|||
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 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 fail_json
|
||||
from library.module_utils.network.f5.icontrol import tmos_version
|
||||
from library.module_utils.network.f5.icontrol import upload_file
|
||||
except ImportError:
|
||||
from ansible.module_utils.network.f5.bigip import F5RestClient
|
||||
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 fail_json
|
||||
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):
|
||||
|
@ -297,6 +334,14 @@ class Parameters(AnsibleF5Parameters):
|
|||
result.append(value)
|
||||
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):
|
||||
@property
|
||||
|
@ -784,6 +829,12 @@ class PartitionedManager(BaseManager):
|
|||
|
||||
class RootUserManager(BaseManager):
|
||||
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
|
||||
result = dict()
|
||||
state = self.want.state
|
||||
|
@ -806,15 +857,126 @@ class RootUserManager(BaseManager):
|
|||
return True
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
def update_on_device(self):
|
||||
escape_patterns = r'([$' + "'])"
|
||||
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)
|
||||
cmd = '-c "printf \\\"{0}\\\" | tmsh modify auth password root"'.format(command)
|
||||
|
||||
# Decrypting logic
|
||||
#
|
||||
# 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(
|
||||
command='run',
|
||||
|
@ -839,6 +1001,74 @@ class RootUserManager(BaseManager):
|
|||
raise F5ModuleError(resp.content)
|
||||
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):
|
||||
def __init__(self):
|
||||
|
|
Loading…
Reference in a new issue