diff --git a/octavia/common/base_taskflow.py b/octavia/common/base_taskflow.py index 920ca5ec9c..8523d88002 100644 --- a/octavia/common/base_taskflow.py +++ b/octavia/common/base_taskflow.py @@ -24,6 +24,7 @@ from oslo_utils import uuidutils from taskflow.conductors.backends import impl_blocking from taskflow import engines from taskflow import exceptions as taskflow_exc +from taskflow.jobs.base import Job from taskflow.listeners import base from taskflow.listeners import logging from taskflow.persistence import models @@ -51,6 +52,44 @@ def retryMaskFilter(record): LOG.logger.addFilter(retryMaskFilter) +def _details_filter(obj): + if isinstance(obj, dict): + ret = {} + for key in obj: + if (key in ('certificate', 'private_key', 'passphrase') and + isinstance(obj[key], str)): + ret[key] = '***' + elif key == 'intermediates' and isinstance(obj[key], list): + ret[key] = ['***'] * len(obj[key]) + else: + ret[key] = _details_filter(obj[key]) + return ret + if isinstance(obj, list): + return [_details_filter(e) for e in obj] + return obj + + +class FilteredJob(Job): + def __str__(self): + # Override the detault __str__ method from taskflow.job.base.Job, + # filter out private information from details + cls_name = type(self).__name__ + details = _details_filter(self.details) + return "%s: %s (priority=%s, uuid=%s, details=%s)" % ( + cls_name, self.name, self.priority, + self.uuid, details) + + +class JobDetailsFilter(log.logging.Filter): + def filter(self, record): + # If the first arg is a Job, convert it now to a string with our custom + # method + if isinstance(record.args[0], Job): + arg0 = record.args[0] + record.args = (FilteredJob.__str__(arg0),) + record.args[1:] + return True + + class BaseTaskFlowEngine(object): """This is the task flow engine @@ -129,6 +168,11 @@ class TaskFlowServiceController(object): def __init__(self, driver): self.driver = driver + # Install filter for taskflow executor logger + taskflow_logger = log.logging.getLogger( + "taskflow.conductors.backends.impl_executor") + taskflow_logger.addFilter(JobDetailsFilter()) + def run_poster(self, flow_factory, *args, **kwargs): with self.driver.persistence_driver.get_persistence() as persistence: with self.driver.job_board(persistence) as job_board: diff --git a/octavia/tests/unit/common/test_base_taskflow.py b/octavia/tests/unit/common/test_base_taskflow.py index 424a91d2a0..779e9d0035 100644 --- a/octavia/tests/unit/common/test_base_taskflow.py +++ b/octavia/tests/unit/common/test_base_taskflow.py @@ -18,6 +18,7 @@ from unittest import mock from oslo_config import cfg from oslo_config import fixture as oslo_fixture from taskflow import engines as tf_engines +from taskflow.jobs.base import Job from octavia.common import base_taskflow import octavia.tests.unit.base as base @@ -173,3 +174,62 @@ class TestTaskFlowServiceController(base.TestCase): job1.extend_expiry.assert_called_once_with(30) job2.extend_expiry.assert_not_called() job3.extend_expiry.assert_not_called() + + +class TestJobDetailsFilter(base.TestCase): + def test_filter(self): + log_filter = base_taskflow.JobDetailsFilter() + + tls_container_data = { + 'certificate': '', + 'private_key': '', + 'passphrase': '', + 'intermediates': [ + '', + '' + ] + } + + job = mock.Mock(spec=Job) + job.details = { + 'store': { + 'listeners': [ + { + 'name': 'listener_name', + 'default_tls_container_data': tls_container_data + } + ], + 'any_recursive': { + 'type': [ + { + 'other_list': [ + tls_container_data, + { + 'test': tls_container_data, + } + ] + } + ] + } + } + } + + record = mock.Mock() + record.args = (job, 'something') + + ret = log_filter.filter(record) + self.assertTrue(ret) + + self.assertNotIn(tls_container_data['certificate'], record.args[0]) + self.assertNotIn(tls_container_data['private_key'], record.args[0]) + self.assertNotIn(tls_container_data['passphrase'], record.args[0]) + self.assertNotIn(tls_container_data['intermediates'][0], + record.args[0]) + self.assertNotIn(tls_container_data['intermediates'][1], + record.args[0]) + self.assertIn('listener_name', record.args[0]) + + record.args = ('arg1', 2) + + ret = log_filter.filter(record) + self.assertTrue(ret) diff --git a/releasenotes/notes/filter-out-private-information-from-taskflow-logs-0d8697140423b4d5.yaml b/releasenotes/notes/filter-out-private-information-from-taskflow-logs-0d8697140423b4d5.yaml new file mode 100644 index 0000000000..5cead5933e --- /dev/null +++ b/releasenotes/notes/filter-out-private-information-from-taskflow-logs-0d8697140423b4d5.yaml @@ -0,0 +1,12 @@ +--- +security: + - | + Filter out private information from the taskflow logs when ''INFO'' level + messages are enabled and when jobboard is enabled. Logs might have included + TLS certificates and private_key. By default, in Octavia only WARNING and + above messages are enabled in taskflow and jobboard is disabled. +fixes: + - | + The parameters of a taskflow Flow were logged in ''INFO'' level messages by + taskflow, it included TLS-enabled listeners and pools parameters, such as + certificates and private_key.