diff --git a/devstack/lib/ironic b/devstack/lib/ironic index 1292390c0c..b65590e81b 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -1359,9 +1359,9 @@ function configure_ironic_api { configure_auth_token_middleware $IRONIC_CONF_FILE ironic $IRONIC_AUTH_CACHE_DIR/api if [[ "$IRONIC_USE_WSGI" == "True" ]]; then - iniset $IRONIC_CONF_FILE api public_endpoint $IRONIC_SERVICE_PROTOCOL://$IRONIC_HOSTPORT + iniset $IRONIC_CONF_FILE oslo_middleware enable_proxy_headers_parsing True elif is_service_enabled tls-proxy; then - iniset $IRONIC_CONF_FILE api public_endpoint $IRONIC_SERVICE_PROTOCOL://$IRONIC_HOSTPORT + iniset $IRONIC_CONF_FILE oslo_middleware enable_proxy_headers_parsing True iniset $IRONIC_CONF_FILE api port $IRONIC_SERVICE_PORT_INT else iniset $IRONIC_CONF_FILE api port $IRONIC_SERVICE_PORT diff --git a/ironic/api/app.py b/ironic/api/app.py index 42c0fcd622..46a6333e1a 100644 --- a/ironic/api/app.py +++ b/ironic/api/app.py @@ -19,6 +19,7 @@ import keystonemiddleware.audit as audit_middleware from oslo_config import cfg import oslo_middleware.cors as cors_middleware from oslo_middleware import healthcheck +from oslo_middleware import http_proxy_to_wsgi import osprofiler.web as osprofiler_web import pecan @@ -104,6 +105,10 @@ def setup_app(pecan_config=None, extra_hooks=None): if CONF.profiler.enabled: app = osprofiler_web.WsgiMiddleware(app) + # NOTE(pas-ha) this registers oslo_middleware.enable_proxy_headers_parsing + # option, when disabled (default) this is noop middleware + app = http_proxy_to_wsgi.HTTPProxyToWSGI(app, CONF) + # add in the healthcheck middleware if enabled # NOTE(jroll) this is after the auth token middleware as we don't want auth # in front of this, and WSGI works from the outside in. Requests to diff --git a/ironic/api/hooks.py b/ironic/api/hooks.py index f5dc5705eb..9ce7b0f797 100644 --- a/ironic/api/hooks.py +++ b/ironic/api/hooks.py @@ -175,5 +175,8 @@ class PublicUrlHook(hooks.PecanHook): """ def before(self, state): - state.request.public_url = (cfg.CONF.api.public_endpoint - or state.request.host_url) + if cfg.CONF.oslo_middleware.enable_proxy_headers_parsing: + state.request.public_url = state.request.application_url + else: + state.request.public_url = (cfg.CONF.api.public_endpoint + or state.request.host_url) diff --git a/ironic/conf/api.py b/ironic/conf/api.py index 85bfc94648..9dee76183a 100644 --- a/ironic/conf/api.py +++ b/ironic/conf/api.py @@ -36,7 +36,10 @@ opts = [ " If None the links will be built using the request's " "host URL. If the API is operating behind a proxy, you " "will want to change this to represent the proxy's URL. " - "Defaults to None.")), + "Defaults to None. " + "Ignored when proxy headers parsing is enabled via " + "[oslo_middleware]enable_proxy_headers_parsing option.") + ), cfg.IntOpt('api_workers', help=_('Number of workers for OpenStack Ironic API service. ' 'The default is equal to the number of CPUs available ' @@ -48,8 +51,10 @@ opts = [ "requests via HTTPS instead of HTTP. If there is a " "front-end service performing HTTPS offloading from " "the service, this option should be False; note, you " - "will want to change public API endpoint to represent " - "SSL termination URL with 'public_endpoint' option.")), + "will want to enable proxy headers parsing with " + "[oslo_middleware]enable_proxy_headers_parsing " + "option or configure [api]public_endpoint option " + "to set URLs in responses to the SSL terminated one.")), cfg.BoolOpt('restrict_lookup', default=True, help=_('Whether to restrict the lookup API to only nodes ' diff --git a/ironic/tests/unit/api/test_proxy_middleware.py b/ironic/tests/unit/api/test_proxy_middleware.py new file mode 100644 index 0000000000..f7a0ac3700 --- /dev/null +++ b/ironic/tests/unit/api/test_proxy_middleware.py @@ -0,0 +1,54 @@ + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Tests to assert that proxy headers middleware works as expected. +""" +from oslo_config import cfg + +from ironic.tests.unit.api import base + + +CONF = cfg.CONF + + +class TestProxyHeadersMiddleware(base.BaseApiTest): + """Provide a basic smoke test to ensure proxy headers middleware works.""" + + def setUp(self): + CONF.set_override('public_endpoint', 'http://spam.ham/eggs', + group='api') + self.proxy_headers = {"X-Forwarded-Proto": "https", + "X-Forwarded-Host": "mycloud.com", + "X-Forwarded-Prefix": "/ironic"} + super(TestProxyHeadersMiddleware, self).setUp() + + def test_proxy_headers_enabled(self): + """Test enabled proxy headers middleware overriding public_endpoint""" + # NOTE(pas-ha) setting config option and re-creating app + # as the middleware registers its config option on instantiation + CONF.set_override('enable_proxy_headers_parsing', True, + group='oslo_middleware') + self.app = self._make_app() + response = self.get_json('/', path_prefix="", + headers=self.proxy_headers) + href = response["default_version"]["links"][0]["href"] + self.assertTrue(href.startswith("https://mycloud.com/ironic")) + + def test_proxy_headers_disabled(self): + """Test proxy headers middleware disabled by default""" + response = self.get_json('/', path_prefix="", + headers=self.proxy_headers) + href = response["default_version"]["links"][0]["href"] + # check that [api]public_endpoint is used when proxy headers parsing + # is disabled + self.assertTrue(href.startswith("http://spam.ham/eggs")) diff --git a/releasenotes/notes/oslo-proxy-headers-middleware-22188a2976f8f460.yaml b/releasenotes/notes/oslo-proxy-headers-middleware-22188a2976f8f460.yaml new file mode 100644 index 0000000000..fc4d4ae2b2 --- /dev/null +++ b/releasenotes/notes/oslo-proxy-headers-middleware-22188a2976f8f460.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Ironic API service now supports HTTP proxy headers parsing + with the help of oslo.middleware package, enabled via new option + ``[oslo_middleware]/enable_proxy_headers_parsing`` (``False`` by default). + + This enables more complex setups of Ironic API service, for example when + the same service instance serves both internal and public API endpoints + via separate proxies. + + When proxy headers parsing is enabled, the value of + ``[api]/public_endpoint`` option is ignored. diff --git a/tools/config/ironic-config-generator.conf b/tools/config/ironic-config-generator.conf index ca5feb58f3..def7f4f700 100644 --- a/tools/config/ironic-config-generator.conf +++ b/tools/config/ironic-config-generator.conf @@ -13,6 +13,7 @@ namespace = oslo.db namespace = oslo.messaging namespace = oslo.middleware.cors namespace = oslo.middleware.healthcheck +namespace = oslo.middleware.http_proxy_to_wsgi namespace = oslo.concurrency namespace = oslo.policy namespace = oslo.log