On 18 October 2026 I fetched the live /.well-known/security.txt from three large web projects. WordPress.org carried Expires: 2027-06-30T15:00:00.000Z, the Rails site carried Expires: 2027-08-13T00:00:00.000Z, and Vercel carried Expires: 2027-09-28T12:00:00.000Z. All three returned 200 with a text/plain content type, and all three set an expiry less than a year out, which is what RFC 9116 recommends.
The file is easy to write. The hard part is getting your framework or host to serve a dot-prefixed folder as plain text. Here is how that goes on the stacks small teams actually run.
What's affected
Any site where a researcher might look for a reporting channel. RFC 9116 requires a Contact field and an Expires field, and says the file belongs at /.well-known/security.txt over HTTPS, with a top-level /security.txt allowed only for legacy reasons. It also tells researchers to be wary of redirects that point somewhere an attacker controls, so serve the file directly from your own hostname.
A good model is the WordPress.org file. It points to a reporting program, lists a policy URL and an acknowledgments page, and adds comments that route people with other problems elsewhere: plugin issues go to the plugins team, and hacked sites go to the site owner or host. That last part saves a small company a pile of misdirected mail.
Here is the file used in every example below.
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/security
How to check if you're exposed
Run this against production, not your repo. Framework and host behavior only shows up on the deployed URL.
URL=https://example.com/.well-known/security.txt
curl -sIL "$URL" | grep -i -E '^HTTP|^location|^content-type'
curl -sL "$URL" | grep -i -E '^(Contact|Expires):'
You want one final 200, a text/plain content type, at least one Contact line and an Expires date in the future. A 200 with text/html means your app is answering the unknown path with a page. A 404 usually means the build or deploy step dropped the dot folder.
What to do now
-
Next.js. The Next.js docs say files in the
publicfolder are served from the base URL, sopublic/.well-known/security.txtmaps to/.well-known/security.txt. The docs also note the default header for these files isCache-Control: public, max-age=0, so a renewal shows up right away. The Next.js docs do not mention dot folders, so test on your host after deploy. -
Rails. Put the file at
public/.well-known/security.txt. The Rails configuration guide saysconfig.public_file_server.enabled"Defaults totrue" and should be set tofalseif NGINX or Apache serves static files instead. Checkconfig/environments/production.rb, because older generated apps tied it to an environment variable. If nginx fronts your app, add a location block for the path, as in the example below.
location = /.well-known/security.txt {
default_type text/plain;
charset utf-8;
alias /var/www/app/public/.well-known/security.txt;
}
-
WordPress. Core has no setting for it. A request for security.txt support in core (Trac ticket 45964) was closed as "maybelater". Your options are a real file at
/.well-known/security.txtin the web root, or a plugin such as Security.txt Manager, which serves both paths and requires rewrites to be enabled. On a subfolder multisite install the plugin only works for the main site. On managed hosting you may not be able to write to the web root, so use the plugin there. -
Hugo. Anything in the
staticdirectory is copied as is, so usestatic/.well-known/security.txt. -
Jekyll. Dotfiles and dot folders are excluded by default. The Jekyll docs describe
includeas forcing inclusion and use.htaccessas the example, so add this to_config.yml:
include:
- .well-known
-
Eleventy. Passthrough copy filters out dot files by default (
copyOptions: { dot: false }in the docs), and the docs do not say how that applies to a.well-knowndirectory. Add the passthrough, build, and look inside the output folder before you trust it. If it is missing, keep the file in a normal folder and let the host map the path. -
Netlify. A
_headersfile goes in the publish directory. To pin the content type and charset:
/.well-known/security.txt
Content-Type: text/plain; charset=utf-8
- Set
Expiresabout six months out and put the renewal on a team calendar.
How to avoid this trap
- Never auto-generate
Expiresas "today plus 180 days" on every build. A date that always moves forward hides the fact that nobody reads the mailbox. - Put an external check on the live URL in CI or a scheduled job. Fail when fewer than 30 days remain.
- Point
Contactat a shared mailbox, not a person. - If you add a single page app, exclude
/.well-known/from the catch-all route so it cannot return your index page.
Why it matters
A missing or expired file tells a researcher that nobody is listening, and some will not bother to guess an address. Customers who ask for a vulnerability disclosure policy also check that the contact is reachable, and the file is the cheapest proof.
The file does not replace a policy. It is the machine-readable signpost, and the Policy line is where the promises live.
Quick check
Your Jekyll site builds fine but the live URL returns 404. What is the likely cause?
Jekyll excludes dot folders by default, so .well-known was not copied into the output. Add it under include in _config.yml.
Why is a 200 response with text/html a failure?
RFC 9116 expects text/plain. An HTML response usually means a catch-all route returned the app shell, so no researcher or tool can read the fields.
How far in the future should Expires be?
RFC 9116 recommends less than a year, to avoid staleness.
Sources
- RFC 9116: A File Format to Aid in Security Vulnerability Disclosure
- WordPress.org security.txt
- Rails security.txt
- Vercel security.txt
- Next.js docs: public folder
- Rails Guides: Configuring Rails Applications
- WordPress Trac ticket 45964
- Security.txt Manager plugin
- Hugo: directory structure
- Jekyll: configuration options
- Eleventy: passthrough file copy
- Netlify: custom headers