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']),