Stripe Webhook Signature issue in Django

In the 3ee Games production setup using Nginx, Gunicorn with Uvicorn workers, and Docker Compose, I encountered an issue where the Stripe webhook handler was raising a SignatureVerificationError because the expected Stripe-Signature header was missing. This article explains the root cause of the issue and the steps to take to resolve it.

The Problem: Header Stripping by Nginx

Stripe sends webhook events with a critical header named Stripe-Signature that allows your backend to verify the authenticity of the request. In the original implementation, the Django view (via the drf-stripe package) attempted to access this header via request.META['HTTP_STRIPE_SIGNATURE']. However, in production the header was not present, leading to an error.

Why Headers Were Missing

Nginx, by default, may drop or rename headers that contain underscores. In this case, it was stripping out the Stripe-Signature header or not passing it through to the upstream Gunicorn server. Additionally, Gunicorn needed to be configured to correctly handle forwarded headers from Nginx.

The Fix: Proper Header Forwarding

1. Update Nginx Configuration

I modified the Nginx configuration to explicitly forward the Stripe-Signature header to the Django application. In the Nginx location block handling the webhook endpoint:

location /some-webhook/ {
    proxy_pass http://your-app-name:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Stripe-Signature $http_stripe_signature;
}

This ensured that even if Nginx would normally filter out headers with underscores, it explicitly passes along the Stripe-Signature header.

2. Configure Gunicorn to Handle Forwarded Headers

Since 3ee Games is using Gunicorn with Uvicorn workers, I needed to ensure that Gunicorn accepted the forwarded headers from Nginx. The Gunicorn command in the Docker Compose file was updated to include the --forwarded-allow-ips="*" flag:

command: gunicorn sockets.asgi:application -k uvicorn.workers.UvicornWorker -b ipaddress:port --forwarded-allow-ips="*"

The forwarded-allow-ips flag instructs Gunicorn to trust and process headers such as X-Forwarded-For and custom headers forwarded from Nginx.

Using the wildcard (*) for Gunicorn’s —forwarded-allow-ips tells Gunicorn to trust any proxy that forwards headers. This is generally safe if your application is only accessible via a trusted proxy (Nginx) and the network configuration prevents direct client access to Gunicorn.

If your infrastructure ensures that all incoming requests pass through Nginx, then using * is acceptable because you rely on the proxy to sanitize and manage the headers. However, if there’s any risk that clients could bypass your reverse proxy and access Gunicorn directly, trusting all forwarded IPs could allow attackers to spoof headers such as X-Forwarded-For, potentially interfering with logging or other logic that depends on the client’s IP address.

For production, it’s best practice to restrict trusted proxies to known IP ranges if possible. In containerized environments, internal networking or firewall rules ensure that Gunicorn is only reachable from the proxy, which makes using * is a safe option.

3. Verification

After making these changes, time to push to the staging enviroment where the stack runs locally:

  • Restarted Docker containers to apply the new configurations.
  • Tested the webhook endpoint by sending test events from Stripe.
  • Logging was added to Django view to confirm that the Stripe-Signature header was present and correctly forwarded.

Once verified, the webhook signature could be successfully validated by Stripe’s library and the error was resolved without needing monkey patches.

Conclusion

The issue was ultimately caused by Nginx not forwarding headers containing underscores. By explicitly setting proxy_set_header Stripe-Signature $http_stripe_signature; in our Nginx configuration and ensuring Gunicorn accepted forwarded headers, we restored the expected behavior. This approach is generally preferable in production environments as it maintains the integrity of the original library code while properly addressing the environment-specific header handling.

This experience underscores the importance of verifying header transmission in a multi-tiered deployment, especially when dealing with external services like Stripe that rely on header-based security.