How to Override the gunicorn Server Header

2021-01-03 A sheepicorn, and I swear it was green.

In all current releases of the popular WSGI server gunicorn, the Server header reports the complete version of gunicorn. I spotted this on my new project DB Buddy. For example, with httpie to check the response headers:

$ http https://db-buddy.herokuapp.com -ph
HTTP/1.1 200 OK
...
Server: gunicorn/20.0.4
...

Reporting the version of server software is not recommended as it is a security risk. Fastly list Server and other vanity headers first in their article The headers we don’t want.

In many setups, gunicorn’s Server header will be overwritten. For example if you’re using Nginx, it will replace Server with its own version (disable that with its server_tokens directive). But my app is running on Heroku which preserves the gunicorn Server header.

Because of the security risk, there has been a long ongoing gunicorn issue to remove the version from the gunicorn header, leaving it as Server: gunicorn. The Pull Request to remove the version was merged nearly a year ago but is still pending release. Until then, we can use the workaround suggested in the original issue: monkey-patch the SERVER_SOFTWARE attribute that gunicorn uses to fill in the Server header.

I’m configuring gunicorn with a submodule of my app’s package, db_buddy/gunicorn.py. So this is where I add the recommended monkey-patch:

# Gunicorn configuration file
# https://docs.gunicorn.org/en/stable/configure.html#configuration-file
# https://docs.gunicorn.org/en/stable/settings.html
import gunicorn


max_requests = 1000
max_requests_jitter = 50

log_file = "-"

if gunicorn.version_info != (20, 0, 4):  # pragma: no cover
    raise ValueError(
        "This monkey patch will probably need removing on later versions due"
        + " to release of https://github.com/benoitc/gunicorn/pull/2233"
    )
gunicorn.SERVER_SOFTWARE = "gunicorn"

I added a version check before the monkey-patch. I normally do such a check when monkey-patching to add behaviour expected in a future upstream release.

(N.B. I use max_requests to avoid memory leaks.)

I run gunicorn like this:

$ gunicorn --config python:db_buddy.gunicorn db_buddy.wsgi

And indeed I can now see the changed Server header:

$ http localhost:8000 -ph
HTTP/1.1 200 OK
...
Server: gunicorn
...

Testing

I like tests, so I also have test coverage for my gunicorn config file. gunicorn provides the ability to validate its configuration with gunicorn --check-config. To avoid the need to run this command separately to my tests, and to ensure its use of the config file appears in my test coverage, I invoke the internal gunicorn CLI function. I then assert that it tries to exit with an expected status code:

import sys
from unittest import mock

import gunicorn
import pytest
from gunicorn.app.wsgiapp import run

from db_buddy.test import SimpleTestCase


class GunicornConfigTests(SimpleTestCase):
    def test_config_imports(self):
        argv = [
            "gunicorn",
            "--check-config",
            "--config",
            "python:db_buddy.gunicorn",
            "db_buddy.wsgi",
        ]
        with pytest.raises(SystemExit) as excinfo, mock.patch.object(sys, "argv", argv):
            run()

        assert excinfo.value.args[0] == 0

Fin

I hope this helps you configure your gunicorn,

—Adam


Working on a Django project? Check out my book Speed Up Your Django Tests which covers loads of best practices so you can write faster, more accurate tests.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django, python