When a security researcher finds a vulnerability in your site, their first problem is not technical — it's logistical: who do I tell, how, and under what rules? Without an answer, the report may arrive by LinkedIn message to someone who left the company in 2022, by opening a public GitHub issue with full exploit details, or not at all — with the details going to a broker instead. security.txt (RFC 9116) is the standardized answer: a small text file at a well-known location that tells researchers how to reach you and what to expect.
It is also one of the most commonly incomplete security files on the web. Files exist with no expiry date (permanently stale by definition), with contacts that are a black-hole web form, with no policy link so researchers don't know whether they're welcome. This guide covers the standard precisely — required fields, placement, lifetimes — and the operational side that the file can't encode but the Policy field should point to. The free security scan checks for the file at both RFC-mandated locations and validates its required fields.
Table of contents
- What security.txt is and where it lives
- The required fields, field by field
- The mistakes that defeat the purpose
- A complete example
- Running the channel the file promises
- Frequently asked questions
What security.txt is and where it lives
security.txt is an IETF standard (RFC 9116, published 2021, evolving from earlier draft practice) that defines a machine-parsable contact file for security issues. It's deliberately boring: a key/value text format, like robots.txt for vulnerability reporting.
Placement matters and is stricter than most people assume. The file must be served at /.well-known/security.txt under the root of the domain — per RFC 9116, that is the canonical location. The standard also notes the legacy root path /security.txt is used in the wild, and many implementations (including our scan) check both — but if you serve only the root path, you're out of spec and some researchers' tooling will miss it. Serve the .well-known path; a redirect from the root path is fine.
The content type should be text/plain, served over HTTPS only — a security.txt delivered over plaintext is a sign error away from being attacker-replaced (imagine a file that redirects researchers to attacker@example.com; the HTTPS requirement exists precisely so the contact channel inherits TLS's authentication).
A minimal, valid file is four lines:
Contact: mailto:security@example.com
Expires: 2027-06-01T00:00:00.000Z
Policy: https://example.com/security-policy
Canonical: https://example.com/.well-known/security.txt
That's the entire technical bar — which is why an incomplete file is such a pure signal of process debt: it takes one meeting to fix, forever.
The required fields, field by field
Our scan validates the file against the RFC's required trio — Contact, Expires, Policy — and reports exactly which are missing. Here's what each must contain to actually work:
Contact (required)
One or more ways to reach a human who can act on security reports. Best practice is an email at a domain you control — mailto:security@example.com — because it's push, asynchronous, and works for researchers in any timezone:
Contact: mailto:security@example.com
Contact: https://example.com/security-contact-form
Judgment calls the scan doesn't make but you should:
- A monitored mailbox beats a web form. Forms are fine as a secondary channel, but a form-only contact means the researcher's report passes through whatever auto-triage the form has, and CSRF/anti-bot walls regularly eat long technical reports. If you use a form, keep the mailto too.
- Never a personal address.
Contact: mailto:dave@example.comfails the moment Dave changes teams. A role alias (security@) survives reorganizations and lets you audit who's behind it. - Format matters:
mailto:URIs for email,https://URLs for web forms. Plainsecurity@example.comwithout the scheme is common and technically non-conformant.
Expires (required)
The date after which the file's data must be considered stale and researchers should assume the channel is unmaintained. The RFC requires it because organizations dissolve, domains change hands, and an orphaned contact file that silently points at a dead mailbox is worse than none — it tells researchers they tried.
- Use a full ISO 8601 timestamp:
Expires: 2027-06-01T00:00:00.000Z. - Renew it on a calendar — one year is typical. An expired security.txt is a "you had one job" signal to every researcher who checks; a recurring reminder is the entire maintenance burden of the standard.
- Some organizations add
Expiresreminders to the same review cycle as certificate renewals, since both are "goes stale, causes public embarrassment" artifacts.
Policy (required)
A URL to your vulnerability disclosure policy — the document that answers what the file structurally can't: What's in scope? Are you offering bounties or thanks? What's the safe-harbor stance for good-faith research? What should a report contain? How long until first response?
This is the field most often shipped as a dead link, and it's the one researchers read first — it determines whether they engage at all. The page it points to is covered in the operations section below.
Recommended optional fields
Encryption: https://example.com/security-pgp.txt
Acknowledgments: https://example.com/security-hall-of-fame
Preferred-Languages: en, de
Canonical: https://example.com/.well-known/security.txt
Hiring: https://example.com/jobs
- Encryption — your PGP key (or age/S/MIME equivalent) for reports containing sensitive details. Optional but signals seriousness.
- Acknowledgments — a researcher hall of fame. Cheap to maintain, disproportionately improves future cooperation.
- Preferred-Languages — the languages your team handles reports in.
- Canonical — the URL this file should be fetched from; helps when the file is reachable at multiple paths (root + .well-known) so tooling can deduplicate.
- Hiring — explicitly not for security reports; its presence stops recruiters from using the security channel, which is a real and ongoing problem for every monitored
security@inbox.
The mistakes that defeat the purpose
From the outside, security.txt looks like a checkbox. These are the ways the checkbox gets checked without the outcome:
- No
Expires— the most common gap (and the one our scan names first when reporting an incomplete file). Permanently-valid contact files are how dead mailboxes masquerade as live channels. - Missing
Policy— researchers land with no idea of scope or safe harbor. Many will simply not engage; "no policy" reads as "no process," fairly or not. - Contact = unmonitored alias.
security@that nobody owns, a ticket queue with no security SLA, or a personal inbox. The file's promise is only as good as the mailbox's staffing. - Serving only at the root path. Out of spec; RFC-strict tooling misses it. Serve
/.well-known/security.txt. - Served over HTTP or with a broken chain. The file inherits the transport problem it exists to solve.
- The Policy page is a wall of legal text with no scope. A policy that says "unauthorized testing is prohibited, all vulnerabilities must be reported through legal channels" is, functionally, a declaration that researchers should go away — and they do. A useful policy is specific about scope and safe harbor; length is not the goal.
- No response capability. The file creates an expectation; if reports land in a void for three months, researchers publish without you. Committing to a first-response time (even "72 hours, automated ack") is the difference between disclosure with you and disclosure about you.
The scan reports the file as a pass only when all three required fields are present, and as a Low finding naming the missing fields otherwise — because a half-file is better than nothing (it shows intent) but doesn't function as a disclosure channel.
A complete example
# security.txt — https://example.com/.well-known/security.txt
Contact: mailto:security@example.com
Contact: https://example.com/security-contact
Encryption: https://example.com/.well-known/pgp-key.txt
Expires: 2027-01-15T00:00:00.000Z
Preferred-Languages: en, de
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/security-policy
Acknowledgments: https://example.com/security-acknowledgments
Serving it from nginx is a static file plus nothing else:
location = /.well-known/security.txt {
default_type text/plain;
return 200 "Contact: mailto:security@example.com
Expires: 2027-01-15T00:00:00.000Z
Policy: https://example.com/security-policy
Canonical: https://example.com/.well-known/security.txt
";
}
(or keep the file on disk and let try_files serve it — just confirm it's reachable over HTTPS at both RFC locations).
Running the channel the file promises
The file is 10% of the work; the channel is the other 90%. A functioning disclosure process has five moving parts:
- A staffed inbox.
security@mapped to at least two people, with an explicit internal SLA: acknowledge in 72 hours, triage in a week. Auto-acknowledgment is fine; silence is not. - A written policy page that answers, in this order: scope (which domains/apps), what researchers may do (safe harbor: no legal action for good-faith testing within scope), how to report, response timeline, and disclosure coordination ("we'll credit you, we coordinate timing"). Keep it to one page.
- Triage and tracking. Reports enter your normal ticketing with a security tag. Severity-assess like any other bug; the difference is coordination on disclosure timing.
- A acknowledgment path — public credit (with permission) or a private thank-you. The acknowledgments page compounds: researchers talk to each other.
- An expiry reminder — the
Expiresdate in a shared calendar, with a 30-day heads-up. This is the entire long-term maintenance cost of RFC 9116 compliance.
For organizations that want the whole framework pre-built, ISO/IEC 29147 (vulnerability disclosure) and 30111 (handling) formalize all of the above; security.txt + a one-page policy is the 80% version that most sites should implement this week.
Frequently asked questions
Is security.txt legally required?
No. It's a voluntary standard — though some procurement and government frameworks increasingly expect a documented disclosure channel, and the file is the cheapest possible form of one.
What's the difference between security.txt and a bug bounty?
The file is the contact info; a bounty is a payment program. You can have a disclosure channel with no bounties (common and fine) — but a bounty without a reachable contact channel is a program researchers can't use.
Does security.txt belong on every subdomain?
The RFC places it at the domain level; a subdomain inherits nothing automatically, and scanners check per-host. If api.example.com and www.example.com are operated by different teams, give each its own file (with team-specific contacts); otherwise a redirect from subdomains to the canonical file is acceptable and common.
We already have /.well-known/security-contact from an older draft — does that count?
No. Older drafts used security.txt-adjacent names (/.well-known/security was once proposed); the standard is exactly /.well-known/security.txt. Old paths can keep working via redirect, but the canonical file must be at the standard path.
What should go in the report-acknowledgment email?
Confirmation of receipt, a tracking reference, your expected triage time, and a safe-harbor restatement ("good-faith research within scope will not be met with legal action"). Researchers who feel process work smoothly become your long-term allies.
Run the free security scan to see whether your security.txt exists, where it's served from, and which required fields it's missing — alongside robots.txt, sitemap.xml and the rest of the checks.
Remote Daemon