From 5f61266f2616f640169e0a8f892b856fbe1d466e Mon Sep 17 00:00:00 2001 From: Myles Penner Date: Fri, 24 May 2024 15:38:11 -0700 Subject: [PATCH] Add keystone audit middleware API logging This commit adds Keystone audit middleware API logging to the Heat charm in versions Yoga and newer to allow users to configure their environment for CADF compliance. This feature can be enabled/disabled and is set to 'disabled' by default to avoid bloat in log files. The logging output is configured to /var/log/heat/heat-api.log. This commit builds on previous discussions: https://github.com/juju/charm-helpers/pull/808. func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/1212 Closes-Bug: 1856555 Change-Id: Ic611b68f35a36489673e3430dd1abbd5aa752fa7 (cherry picked from commit 69886c1bcd7a5a8e5c92478c9ea1d45801fc8d38) --- config.yaml | 5 ++ hooks/heat_utils.py | 11 ++- templates/yoga/api-paste.ini | 130 ++++++++++++++++++++++++++++++ templates/yoga/api_audit_map.conf | 32 ++++++++ templates/yoga/heat.conf | 107 ++++++++++++++++++++++++ tests/tests.yaml | 4 + unit_tests/test_heat_utils.py | 1 + 7 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 templates/yoga/api-paste.ini create mode 100644 templates/yoga/api_audit_map.conf create mode 100644 templates/yoga/heat.conf diff --git a/config.yaml b/config.yaml index 9ee04c4..17c6e00 100644 --- a/config.yaml +++ b/config.yaml @@ -12,6 +12,11 @@ options: default: False description: | Setting this to True will allow supporting services to log to syslog. + audit-middleware: + type: boolean + default: False + description: | + Enable Keystone auditing middleware for logging API calls. openstack-origin: type: string default: bobcat diff --git a/hooks/heat_utils.py b/hooks/heat_utils.py index f0d4edb..aacc47d 100644 --- a/hooks/heat_utils.py +++ b/hooks/heat_utils.py @@ -120,6 +120,7 @@ SVC = 'heat' HEAT_DIR = '/etc/heat' HEAT_CONF = '/etc/heat/heat.conf' HEAT_API_PASTE = '/etc/heat/api-paste.ini' +HEAT_AUDIT_CONF = '%s/api_audit_map.conf' % HEAT_DIR HAPROXY_CONF = '/etc/haproxy/haproxy.cfg' APACHE_PORTS_CONF = '/etc/apache2/ports.conf' HTTPS_APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend' @@ -147,17 +148,23 @@ CONFIG_FILES = OrderedDict([ context.WorkerConfigContext(), context.BindHostContext(), context.MemcacheContext(), - context.OSConfigFlagContext()], + context.OSConfigFlagContext(), + context.KeystoneAuditMiddleware(service=SVC)] }), (HEAT_API_PASTE, { 'services': [s for s in BASE_SERVICES if 'api' in s], - 'contexts': [HeatIdentityServiceContext()], + 'contexts': [HeatIdentityServiceContext(), + context.KeystoneAuditMiddleware(service=SVC)], }), (HAPROXY_CONF, { 'contexts': [context.HAProxyContext(singlenode_mode=True), HeatHAProxyContext()], 'services': ['haproxy'], }), + (HEAT_AUDIT_CONF, { + 'contexts': [context.KeystoneAuditMiddleware(service=SVC)], + 'services': ['heat-api'] + }), (HTTPS_APACHE_CONF, { 'contexts': [HeatApacheSSLContext()], 'services': ['apache2'], diff --git a/templates/yoga/api-paste.ini b/templates/yoga/api-paste.ini new file mode 100644 index 0000000..62854b6 --- /dev/null +++ b/templates/yoga/api-paste.ini @@ -0,0 +1,130 @@ +# yoga +# heat-api pipeline +[pipeline:heat-api] +{% if audit_middleware and service_name %} +pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation osprofiler authurl authtoken audit context apiv1app +{% else %} +pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation osprofiler authurl authtoken context apiv1app +{% endif %} + + +# heat-api pipeline for standalone heat +# ie. uses alternative auth backend that authenticates users against keystone +# using username and password instead of validating token (which requires +# an admin/service token). +# To enable, in heat.conf: +# [paste_deploy] +# flavor = standalone +# +[pipeline:heat-api-standalone] +{% if audit_middleware and service_name %} +pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authpassword audit context apiv1app +{% else %} +pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authpassword context apiv1app +{% endif %} + +# heat-api pipeline for custom cloud backends +# i.e. in heat.conf: +# [paste_deploy] +# flavor = custombackend +# +[pipeline:heat-api-custombackend] +pipeline = healthcheck cors request_id faultwrap versionnegotiation context custombackendauth apiv1app + +# To enable, in heat.conf: +# [paste_deploy] +# flavor = noauth +# +[pipeline:heat-api-noauth] +pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation noauth context apiv1app + +# heat-api-cfn pipeline +[pipeline:heat-api-cfn] +pipeline = healthcheck cors http_proxy_to_wsgi cfnversionnegotiation osprofiler ec2authtoken authtoken context apicfnv1app + +# heat-api-cfn pipeline for standalone heat +# relies exclusively on authenticating with ec2 signed requests +[pipeline:heat-api-cfn-standalone] +pipeline = healthcheck cors http_proxy_to_wsgi cfnversionnegotiation ec2authtoken context apicfnv1app + +# heat-api-cloudwatch pipeline +[pipeline:heat-api-cloudwatch] +pipeline = healthcheck cors versionnegotiation osprofiler ec2authtoken authtoken context apicwapp + +# heat-api-cloudwatch pipeline for standalone heat +# relies exclusively on authenticating with ec2 signed requests +[pipeline:heat-api-cloudwatch-standalone] +pipeline = healthcheck cors versionnegotiation ec2authtoken context apicwapp + +[app:apiv1app] +paste.app_factory = heat.common.wsgi:app_factory +heat.app_factory = heat.api.openstack.v1:API + +[app:apicfnv1app] +paste.app_factory = heat.common.wsgi:app_factory +heat.app_factory = heat.api.cfn.v1:API + +[app:apicwapp] +paste.app_factory = heat.common.wsgi:app_factory +heat.app_factory = heat.api.cloudwatch:API + +[filter:versionnegotiation] +paste.filter_factory = heat.common.wsgi:filter_factory +heat.filter_factory = heat.api.openstack:version_negotiation_filter + +[filter:cors] +paste.filter_factory = oslo_middleware.cors:filter_factory +oslo_config_project = heat + +[filter:faultwrap] +paste.filter_factory = heat.common.wsgi:filter_factory +heat.filter_factory = heat.api.openstack:faultwrap_filter + +[filter:cfnversionnegotiation] +paste.filter_factory = heat.common.wsgi:filter_factory +heat.filter_factory = heat.api.cfn:version_negotiation_filter + +[filter:cwversionnegotiation] +paste.filter_factory = heat.common.wsgi:filter_factory +heat.filter_factory = heat.api.cloudwatch:version_negotiation_filter + +[filter:context] +paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory + +[filter:ec2authtoken] +paste.filter_factory = heat.api.aws.ec2token:EC2Token_filter_factory + +[filter:http_proxy_to_wsgi] +paste.filter_factory = oslo_middleware:HTTPProxyToWSGI.factory + +# Middleware to set auth_url header appropriately +[filter:authurl] +paste.filter_factory = heat.common.auth_url:filter_factory + +# Auth middleware that validates token against keystone +[filter:authtoken] +paste.filter_factory = keystonemiddleware.auth_token:filter_factory + +# Auth middleware that validates username/password against keystone +[filter:authpassword] +paste.filter_factory = heat.common.auth_password:filter_factory + +# Auth middleware that validates against custom backend +[filter:custombackendauth] +paste.filter_factory = heat.common.custom_backend_auth:filter_factory + +# Auth middleware that accepts any auth +[filter:noauth] +paste.filter_factory = heat.common.noauth:filter_factory + +# Middleware to set x-openstack-request-id in http response header +[filter:request_id] +paste.filter_factory = oslo_middleware.request_id:RequestId.factory + +[filter:osprofiler] +paste.filter_factory = osprofiler.web:WsgiMiddleware.factory + +[filter:healthcheck] +paste.filter_factory = oslo_middleware:Healthcheck.factory + +{% include "section-filter-audit" %} \ No newline at end of file diff --git a/templates/yoga/api_audit_map.conf b/templates/yoga/api_audit_map.conf new file mode 100644 index 0000000..8c0e8e7 --- /dev/null +++ b/templates/yoga/api_audit_map.conf @@ -0,0 +1,32 @@ +[DEFAULT] +# default target endpoint type +# should match the endpoint type defined in service catalog +target_endpoint_type = None + +# possible end path of api requests +[path_keywords] +stacks = stack +resources = resource +preview = None +detail = None +abandon = None +snapshots = snapshot +restore = None +outputs = output +metadata = server +signal = None +events = event +template = None +template_versions = template_version +functions = None +validate = None +resource_types = resource_type +build_info = None +actions = None +software_configs = software_config +software_deployments = software_deployment +services = None + +# map endpoint type defined in service catalog to CADF typeURI +[service_endpoints] +orchestration = service/orchestration \ No newline at end of file diff --git a/templates/yoga/heat.conf b/templates/yoga/heat.conf new file mode 100644 index 0000000..53e2ef4 --- /dev/null +++ b/templates/yoga/heat.conf @@ -0,0 +1,107 @@ +[DEFAULT] +use_syslog = {{ use_syslog }} +debug = {{ debug }} +verbose = {{ verbose }} +log_dir = /var/log/heat +instance_user = {{ instance_user }} +instance_driver = heat.engine.nova +{% if plugin_dirs -%} +plugin_dirs = {{ plugin_dirs }} +{% else -%} +plugin_dirs = /usr/lib64/heat,/usr/lib/heat +{% endif -%} +environment_dir = /etc/heat/environment.d +host = heat +auth_encryption_key = {{ encryption_key }} +deferred_auth_method = trusts +stack_domain_admin = heat_domain_admin +stack_domain_admin_password = {{ heat_domain_admin_passwd }} +stack_user_domain_name = heat +num_engine_workers = {{ workers }} +{%- if max_stacks_per_tenant %} +max_stacks_per_tenant = {{ max_stacks_per_tenant }} +{%- endif %} +{% if sections and 'DEFAULT' in sections -%} +{% for key, value in sections['DEFAULT'] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif %} + +{% if user_config_flags -%} +{% for key, value in user_config_flags.items() -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif -%} + +{% if transport_url %} +transport_url = {{ transport_url }} +{% endif %} + +{% if auth_host -%} +{% include "section-keystone-authtoken-mitaka" %} + +[trustee] +auth_plugin = password +auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }} +username = {{ admin_user }} +password = {{ admin_password }} +user_domain_name = default + +[clients_keystone] +auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }} + +[ec2_authtoken] +auth_uri = {{service_protocol }}://{{ service_host }}:{{ service_port }} +keystone_ec2_uri = {{service_protocol }}://{{ service_host }}:{{ service_port }}/v2.0/ec2tokens +{% endif %} + +{% if database_host -%} +[database] +connection = {{ database_type }}://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}{% if database_ssl_ca %}?ssl_ca={{ database_ssl_ca }}{% if database_ssl_cert %}&ssl_cert={{ database_ssl_cert }}&ssl_key={{ database_ssl_key }}{% endif %}{% endif %} +{% endif %} + +[paste_deploy] +api_paste_config=/etc/heat/api-paste.ini + +[heat_api] +bind_host = {{ bind_host }} +{% if api_listen_port -%} +bind_port={{ api_listen_port }} +{% else -%} +bind_port=8004 +{% endif %} +workers = {{ workers }} + +[heat_api_cfn] +bind_host = {{ bind_host }} +{% if api_cfn_listen_port -%} +bind_port={{ api_cfn_listen_port }} +{% else -%} +bind_port=8000 +{% endif %} +workers = {{ workers }} + +{% include "section-oslo-messaging-rabbit-ocata" %} + +{% if use_internal_endpoints -%} +[clients] +endpoint_type = internalURL + +[clients_heat] +# See LP 1770144 +endpoint_type = publicURL + +{%- endif %} + +{% include "section-oslo-middleware" %} + +{% if sections -%} +{% for section in sections if section != 'DEFAULT' -%} +[{{ section }}] +{% for key, value in sections[section] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endfor -%} +{% endif %} + +{% include "section-audit-middleware-notifications" %} \ No newline at end of file diff --git a/tests/tests.yaml b/tests/tests.yaml index 5129c09..6154d3b 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -16,9 +16,13 @@ configure: tests: - zaza.openstack.charm_tests.heat.tests.HeatBasicDeployment - zaza.openstack.charm_tests.policyd.tests.HeatTests + - zaza.openstack.charm_tests.audit.tests.KeystoneAuditMiddlewareTest tests_options: policyd: service: heat + audit-middleware: + service: heat + force_deploy: - noble-caracal diff --git a/unit_tests/test_heat_utils.py b/unit_tests/test_heat_utils.py index 1445d85..03a496c 100644 --- a/unit_tests/test_heat_utils.py +++ b/unit_tests/test_heat_utils.py @@ -50,6 +50,7 @@ RESTART_MAP = OrderedDict([ ('/etc/heat/heat.conf', ['heat-api', 'heat-api-cfn', 'heat-engine']), ('/etc/heat/api-paste.ini', ['heat-api', 'heat-api-cfn']), ('/etc/haproxy/haproxy.cfg', ['haproxy']), + ('/etc/heat/api_audit_map.conf', ['heat-api']), ('/etc/apache2/sites-available/openstack_https_frontend', ['apache2']), ('/etc/apache2/sites-available/openstack_https_frontend.conf', ['apache2']),