Add security.txt and a Vulnerability Disclosure Process to a Django Site

Publish /.well-known/security.txt from Django with contact, expiry, policy, and encryption fields, set up a vulnerability disclosure policy and intake mailbox, triage reports, and avoid common mistakes like expired files and unmonitored addresses.

Problem statement

A researcher finds a vulnerability in your Django app and wants to tell you. There is no obvious contact, so they write to a generic support address that auto-closes tickets, post on social media, or give up. security.txt (RFC 9116) is a small text file at /.well-known/security.txt that tells people where and how to report security issues. It only helps if a monitored process sits behind it.

Vulnerability disclosureBug foundby a researchersecurity.txtwhere to reportIntakemonitored mailboxTriage and fixacknowledge, patchsecurity.txt is the signpost; the process behind it does the work.
  1. Create a monitored address such as security@example.com routed to people who can triage.
  2. Publish /.well-known/security.txt with Contact, Expires, Policy, and optionally Encryption and Preferred-Languages.
  3. Serve it from Django with a simple view (or from static files via Nginx or WhiteNoise) as text/plain.
  4. Write a short disclosure policy: scope, safe harbor, response times, and what not to do.
  5. Renew the Expires date before it lapses, and test the contact path periodically.

Step-by-step solution

1. Set up intake

Use a dedicated address or form that creates tickets in a security queue, not general support. At least two people should receive it. Optionally publish a PGP key for encrypted reports. Define internal response targets: acknowledge within a few business days, triage within a week.

2. Write the file

Contact: mailto:security@example.com
Expires: 2027-10-01T00:00:00.000Z
Policy: https://example.com/security/policy/
Encryption: https://example.com/.well-known/pgp-key.txt
Preferred-Languages: en, de
Canonical: https://example.com/.well-known/security.txt

Contact and Expires are required by RFC 9116. Keep Expires less than a year in the future so the information stays fresh. Optionally sign the file with OpenPGP.

/.well-known/security.txtContact:mailto:security@example.comExpires:2027-10-01T00:00:00.000ZPolicy:https://example.com/security/policy/Canonical: https://example.com/.well-known/security.txt1231Required; must reach people who triage.2Required; under a year ahead, renew on schedule.3Scope, safe harbor, response times.Contact and Expires are required; the rest make reports betterdjango-deployment.com
An expired file signals an abandoned process to researchers.

3. Serve it from Django

# core/views.py
from django.http import HttpResponse
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_GET

SECURITY_TXT = """Contact: mailto:security@example.com
Expires: 2027-10-01T00:00:00.000Z
Policy: https://example.com/security/policy/
Canonical: https://example.com/.well-known/security.txt
"""

@require_GET
@cache_control(max_age=86400, public=True)
def security_txt(request):
    return HttpResponse(SECURITY_TXT, content_type="text/plain; charset=utf-8")
# urls.py
urlpatterns = [path(".well-known/security.txt", security_txt), ...]

Alternatively, render it from a template so Expires comes from settings, or serve a static file through Nginx. Make sure SECURE_SSL_REDIRECT and ALLOWED_HOSTS do not interfere, and that it is reachable on every public hostname.

4. Publish a disclosure policy

A short page covering:

  • Scope: which domains and apps are in scope; third-party services are not.
  • Safe harbor: you will not pursue legal action for good-faith research within the rules.
  • Rules: no data exfiltration beyond proof, no denial of service, no social engineering, no access to other users' data.
  • Process: how you acknowledge, triage, fix, and credit reporters, and expected timelines.

Whether to offer bounties is a separate decision; a clear policy without rewards still gets reports.

5. Triage and close the loop

Treat each report like an incident: reproduce, assess severity, fix, deploy, and inform the reporter. Track reports in a private issue tracker. Renew Expires on a calendar reminder and send a test email to the contact address quarterly.

from report to fix1Report arrivessecurity mailbox2Acknowledgewithin days3Reproduce and rateseverity, impact4Fix and deploytests, review5Tell the reportercredit if wantedPredictable responses make researchers report to you firstdjango-deployment.com
Rotate secrets and check logs if the report suggests data was exposed.

Explanation

RFC 9116 standardizes a machine-readable file that tells researchers and automated tools how to report vulnerabilities. Its value depends entirely on the process behind it: a monitored contact, clear rules, and timely responses. For a Django site, serving the file is a few lines of code; maintaining the process is the real work, and it pays off by turning potential public disclosures into private reports you can fix first.

Edge cases / notes

  • Multiple domains. Serve the file on every domain, or point Canonical to the main one.
  • Spam. Security mailboxes attract automated "beg bounty" reports; triage quickly and keep standard replies.
  • Legal review. Have safe harbor language reviewed for your jurisdiction.

Next steps

FAQ

Where should security.txt be located?

At /.well-known/security.txt on each public domain, served over HTTPS as plain text.

Which fields are required?

Contact and Expires. Policy, Encryption, and Canonical are recommended.

Does security.txt replace a bug bounty program?

No. It points to your reporting process, which may or may not include rewards.

How often should I update it?

Before the Expires date, at least yearly, and whenever the contact or policy changes.

Learning path · step 15 of 15

Hardening a Django deployment

Keep going