opentelemetry-python-contrib icon indicating copy to clipboard operation
opentelemetry-python-contrib copied to clipboard

DisallowedHost exception for Django instrumentation

Open leohahn opened this issue 2 years ago • 23 comments

Describe your environment

  • Django 4.1.7
  • Python 3.11.1
  • opentelemetry-instrumentation-django 0.38b0

Steps to reproduce Create basic django app:

mkdir django-test && cd django-test
python -m venv .venv
source .venv/bin/activate
pip install django
django-admin startproject django_test .
python manage.py migrate
cat <<EOF > gunicorn.config.py
from opentelemetry.instrumentation.django import DjangoInstrumentor

def post_fork(server, worker):
    DjangoInstrumentor().instrument()
EOF

Only allow hosts from a specific origin. This is best practice in terms of security. Change the following variables in django_test/settings.py:

DEBUG = False
ALLOWED_HOSTS = ['example.com']

Create a health check middleware. The middleware is added at the beginning of the middleware array, since we don't want the allowed hosts rule to apply to the health check, since we'll likely be receiving health checks from the load balancer, which does not have the correct host.

cat <<EOF > django_test/middleware.py
from django.http import HttpResponse


class HealthCheckMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.path == "/api/health":
            return HttpResponse("ok\n")
        return self.get_response(request)
EOF

Add to the array in django_test.settings.py:

MIDDLEWARE = [
    'django_test.middleware.HealthCheckMiddleware',
    # ... other middlewares here
]

Now the health check route works as expected when we run the server:

python manage.py runserver
❯ curl localhost:8000/api/health
ok

Now we need to add open telemetry django and gunicorn:

pip install opentelemetry-instrumentation-django
pip install gunicorn

Now we run django with instrumentation:

DJANGO_SETTINGS_MODULE=django_test.settings OTEL_SERVICE_NAME=TestApi gunicorn django_test.wsgi:application -c gunicorn.config.py

The healthcheck endpoint fails with 400 now, because of the DisallowedHost exception. More specifically, the error is raised here on the request.build_absolute_uri call.

What is the expected behavior?

The healthcheck endpoint should not fail when telemetry is enabled.

I'm not sure what the best solution for this is, but I guess we want to build the absolute URI without calling request.build_absolute_uri, since it will throw an exception.

EDIT: since I'm new to the library, I'm not sure what the best solution would be. But if you have any pointers feel free to say them that I can try to fix the issue as well.

leohahn avatar May 03 '23 18:05 leohahn

Hey @srikanthccv. Can you please assign this to me.

TheAnshul756 avatar May 05 '23 18:05 TheAnshul756

Hello @leohahn, thank you for bringing this issue to our attention. After conducting some research, I have found that the request.build_absolute_uri method is internally calling the get_host method, which checks for allowed hosts and throws an exception if the host is not allowed. This behavior is not ideal, as it is affecting the execution of other middlewares even though it has been added to the top of the middleware stack.

To address this issue, I suggest that we write our own build_absolute_uri method to bypass the Middleware and ensure that the execution of other middlewares is not affected. However, before implementing this solution, we should thoroughly test it to ensure that it does not introduce any new bugs or issues.

I would appreciate your thoughts on this approach, @srikanthccv. Thank you.

TheAnshul756 avatar May 06 '23 12:05 TheAnshul756

How about giving the option to configure where in the middleware stack Django instrumentation be placed? I believe that will also help potential issues that arise because of the order of the execution. For instance, the user has a number of middleware that should be executed before any instrumentation work begins.

srikanthccv avatar May 06 '23 14:05 srikanthccv

Many frameworks provide functionality for getting middleware priority and sorting them by it. However, I'm not sure how we could implement this on the instrumentation level. It's an interesting idea to give users the option to configure where in the middleware stack Django instrumentation should be placed, as this could help prevent potential issues that arise due to the order of execution. We could explore this further and see if there are any feasible solutions.

TheAnshul756 avatar May 08 '23 09:05 TheAnshul756

https://github.com/open-telemetry/opentelemetry-python-contrib/blob/1a1163e919f1e573009b1ba5bb43a7365a80f58f/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/init.py#L332-L363

srikanthccv avatar May 08 '23 10:05 srikanthccv

I appreciate the code you provided to set the order of middleware. However, my concern is how we would decide which middleware should execute before the OpenTelemetry middleware.

I think providing a decorator for middlewares that executes before the OpenTelemetry middleware is a potential solution to give developers more control over the middleware stack. However, I understand that this would require customers to make code-level changes to import and use the decorator. What do you think about this solution?

TheAnshul756 avatar May 08 '23 10:05 TheAnshul756

my concern is how we would decide which middleware should execute before the OpenTelemetry middleware.

We don't; the user decides. The configuration option I mentioned earlier is the order index where the OTEL middleware should go.

srikanthccv avatar May 08 '23 11:05 srikanthccv

Asking this just to check if we are on the same page, Django takes a tuple of middleware in which the customer provides their middleware, and our code by default inserts the OpenTelemetry middleware on top. Could you please explain how the user would specify where to put the OpenTelemetry middleware in this tuple? Would they need to provide an index as input to indicate where the OpenTelemetry middleware should be added?"

TheAnshul756 avatar May 08 '23 12:05 TheAnshul756

it could be the index or name of the middleware to come after/before or by some other way to place it in the right position.

srikanthccv avatar May 08 '23 12:05 srikanthccv

Hey folks, do we have any updates here?

leohahn avatar Jul 24 '23 18:07 leohahn

Hello @leohahn I sincerely apologize for the delay in addressing the issue. Due to unforeseen circumstances, I couldn't work on it as promptly as I had hoped. However, I want to assure you that I am now actively working on resolving the problem and will dedicate my efforts to completing it within next one week.

TheAnshul756 avatar Jul 25 '23 08:07 TheAnshul756

Hey @TheAnshul756, thanks for the response. No worries! Not sure if I can help, but feel free to point something that I could be useful as well if necessary.

leohahn avatar Jul 25 '23 17:07 leohahn

@leohahn @TheAnshul756 Hi guys,

Just ran into the same issue today. I'm using AWS ECS and implemented HealthCheckMiddleware exacly the way described to bypass Django's security check.

Here is my quick workaround for the issue. Add this to your settings.py (originally from https://stackoverflow.com/questions/37031749/django-allowed-hosts-ips-range

from socket import gethostbyname_ex, gethostname

ALLOWED_HOSTS += [ gethostname(),] + list(set(gethostbyname_ex(gethostname())[2]))

This will add the private ip address of ECS container to each app instance which get_host validate against. Adding these hosts to ALLOWED_HOSTS shouldn't impact your security since they are private addresses.

shinkawk avatar Aug 16 '23 03:08 shinkawk

I am also having trouble avoiding ALLOWED_HOSTS errors running on Linux specifically. Even after adding a wildcard to ALLOWED_HOSTS to allow everything, I don't seem to get any spans. I am guessing it is related to this. When running with "python manage.py runserver --noreload" on Windows it works, but gunicorn on Linux does not.

jeremydvoss avatar Nov 02 '23 22:11 jeremydvoss

I want to make sure that whatever solution we come up with here works intuitively for auto-instrumentation. That functionality seems to have been a bit overlooked in the past. So, for instance, we should prioritizing finding a fix that works natively and doesn't require additional user input. If use input is needed, as is the case for the settings module env var, then we need to make sure it's configurable via environment variable so as to not break codeless scenarios.

jeremydvoss avatar Nov 02 '23 22:11 jeremydvoss

Potentially related: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2043

jeremydvoss avatar Nov 07 '23 00:11 jeremydvoss

Hey @srikanthccv. Can you please assign this to me.

Puneet140500 avatar Aug 13 '24 07:08 Puneet140500

@Puneet140500 Please open a PR if you have some work done.

emdneto avatar Aug 13 '24 12:08 emdneto

https://github.com/saichander17/opentelemetry-python-contrib/pull/1 https://github.com/saichander17/opentelemetry-python-contrib/pull/2

Do any of these make sense? I haven't tested or did much. I literally updated those files quickly to get an idea of if any one of them are follows correct approach or not.

cc: @lzchen

saichander17 avatar Sep 11 '24 02:09 saichander17

@saichander17, please open the PR against this repository

emdneto avatar Sep 12 '24 00:09 emdneto

I just don't know if it makes any sense or not. That's why I didn't open the PR against this one. Let me go ahead and do it anyways

saichander17 avatar Sep 12 '24 01:09 saichander17

Done. https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2866 https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2867

saichander17 avatar Sep 12 '24 01:09 saichander17

Seems like #2834 is already in progress and is probably in the right direction. I'll go ahead close my PRs

saichander17 avatar Sep 12 '24 14:09 saichander17