On 18 October 2026 GitHub's live security.txt carried Expires: 2026-11-17T09:05:33z, about 30 days out, while CISA's carried Expires: 2027-10-01T00:00:00.000Z. Both were served at /.well-known/security.txt as text/plain with a UTF-8 charset. Two very different companies, one pattern: the file exists, it parses, and the date is in the future.
That is the bar RFC 9116 sets, and plenty of small companies miss it. An expired or misplaced file is worse than none, because it tells a researcher that nobody is watching.
What's affected
Any organization that wants researchers to find a reporting channel without guessing. RFC 9116 is an Informational RFC, so it is guidance, not law. Its wording still matters because customers and scanners test against it. It defines eight fields. Two are mandatory:
Contactmust be present and may repeat, listed in order of preference. Use amailto:ortel:URI, or an https link to a form.Expiresmust be present exactly once. The RFC recommends a value less than a year in the future.
The rest are optional: Acknowledgments, Canonical, Encryption, Hiring, Policy and Preferred-Languages. CISA's file also has a CSAF line, which is not one of the eight fields in the RFC, so treat that as an extension you do not need.
The file must be reachable at /.well-known/security.txt over https. A copy at /security.txt is allowed for legacy reasons, but if both exist the well-known path wins. The response must have Content-Type: text/plain with the charset set to utf-8.
How to check if you're exposed
curl -sIL https://example.com/.well-known/security.txt | grep -i -E '^HTTP|content-type'
curl -sL https://example.com/.well-known/security.txt
You want a final 200, a text/plain; charset=utf-8 header, a Contact: line and an Expires: line in the future. A 200 with text/html usually means a single page app is returning its index page for an unknown path.
What to do now
- Write the file. A complete working example:
# Report security issues here. Thank you for looking.
Contact: mailto:security@example.com
Contact: https://example.com/security/report
Expires: 2027-04-30T00:00:00z
Preferred-Languages: en
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/vulnerability-disclosure
Acknowledgments: https://example.com/security/thanks
- Serve it with the right header. In nginx:
location = /.well-known/security.txt {
default_type text/plain;
charset utf-8;
alias /var/www/security/security.txt;
}
On static hosts that skip dot directories at build time, add a rule or a copy step, then confirm with curl. Do not rely on a catch-all route.
- Set the date for six months out and put the renewal on a calendar owned by a team, not a person.
- Add the check below to CI so an expiring file fails a build before it embarrasses you.
- If you sign the file, RFC 9116 recommends an OpenPGP cleartext signature and says to include
Canonicalso the signature authenticates the location.
#!/usr/bin/env bash
url="${1:-https://example.com/.well-known/security.txt}"
exp=$(curl -sfL "$url" | grep -i '^Expires:' | head -n 1 | cut -d' ' -f2)
[ -z "$exp" ] && { echo "no Expires field"; exit 1; }
python3 - "$exp" <<'PY'
import sys, datetime
s = sys.argv[1].replace('Z', '+00:00').replace('z', '+00:00')
d = datetime.datetime.fromisoformat(s)
now = datetime.datetime.now(datetime.timezone.utc)
left = int((d.timestamp() - now.timestamp()) / 86400)
print(f"security.txt expires in {left} days")
sys.exit(0 if left > 14 else 1)
PY
How to avoid this trap
- Treat
Expiresas a renewal, not a constant. Generate the file in your build and set the date from the build date plus a fixed window,. - Test the public URL, not the repo copy. Header and routing bugs only show up on the live path.
- Point
Contactat a shared mailbox so staff changes cannot orphan it. - Keep
Canonicalaccurate if you serve the file from more than one hostname.
Why it matters
Under the EU CRA, Annex II asks manufacturers to give users a single point of contact for reporting vulnerabilities and where to find the disclosure policy. That obligation applies from 11 December 2027. A correct security.txt is a cheap way to publish that contact in a machine-readable form, though the regulation does not name the file.
Quick check
Which two security.txt fields are required by RFC 9116?
Contact and Expires. Everything else is optional.
Your file returns 200 but with text/html. What is probably wrong?
A catch-all route or single page app is answering the path. Add an explicit route that serves the file as text/plain with a utf-8 charset.