thoresuenert / tempest-security-txt
Serve an RFC 9116 compliant security.txt file from your Tempest application.
Package info
github.com/thoresuenert/tempest-security-txt
pkg:composer/thoresuenert/tempest-security-txt
Requires
- php: ^8.5
- tempest/clock: ^3.18
- tempest/container: ^3.18
- tempest/core: ^3.18
- tempest/datetime: ^3.18
- tempest/http: ^3.18
- tempest/router: ^3.18
- tempest/support: ^3.18
Requires (Dev)
- phpunit/phpunit: ^12.0
- tempest/framework: ^3.18
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Serve an RFC 9116 compliant security.txt file from your Tempest application.
security.txt is a machine-parsable file that tells security researchers how to report vulnerabilities to you. This package generates and serves it at /.well-known/security.txt — no controller or view needed on your end, thanks to Tempest's discovery.
Installation
composer require thoresuenert/tempest-security-txt ./tempest install security-txt
The installer publishes a security-txt.config.php file into your project:
use Tempest\DateTime\Duration; use Tempest\SecurityTxt\SecurityTxtConfig; return new SecurityTxtConfig( contact: 'mailto:security@your-domain.com', expires: Duration::days(180), cache: Duration::hours(1), );
That's it. Your application now responds to GET /.well-known/security.txt:
Contact: mailto:security@your-domain.com Expires: 2027-02-03T12:00:00Z
The legacy /security.txt path permanently redirects to the well-known location, as described in section 3 of the RFC. When no contact is configured, both routes return a 404 instead of serving an invalid file.
Configuration
All fields defined by RFC 9116 are supported. Repeatable fields accept a single string or an array of strings:
use Tempest\DateTime\Duration; use Tempest\SecurityTxt\SecurityTxtConfig; return new SecurityTxtConfig( contact: [ 'mailto:security@your-domain.com', 'https://your-domain.com/security-contact', ], expires: Duration::days(180), encryption: 'https://your-domain.com/pgp-key.txt', acknowledgments: 'https://your-domain.com/hall-of-fame', policy: 'https://your-domain.com/disclosure-policy', hiring: 'https://your-domain.com/jobs', canonical: 'https://your-domain.com/.well-known/security.txt', preferredLanguages: ['en', 'de'], comment: 'We appreciate responsible disclosure.', cache: Duration::hours(1), );
Expiration
RFC 9116 requires an Expires field and recommends it be less than a year into the future. You can pass a fixed DateTimeInterface, but passing a Duration is usually more convenient: it is resolved relative to the current time on every request, so the file never goes stale.
expires: Duration::days(180),
Extension fields
Fields registered in the IANA security.txt registry after RFC 9116 — or your own — can be added through extensions:
extensions: [
'CSAF' => 'https://your-domain.com/.well-known/csaf/provider-metadata.json',
],
Signing
Section 2.3 of the RFC recommends signing the file with an OpenPGP cleartext signature. Implement the SecurityTxtSigner interface and reference it in your config — it is resolved through the container, so you can inject whatever you need:
use Tempest\SecurityTxt\SecurityTxtSigner; final readonly class PgpSigner implements SecurityTxtSigner { public function sign(string $content): string { // Wrap $content in an OpenPGP cleartext signature… } }
return new SecurityTxtConfig( // … canonical: 'https://your-domain.com/.well-known/security.txt', signer: PgpSigner::class, );
When signing, also set the canonical field so the signature can authenticate the file's location, as the RFC recommends.
Validation
The generator enforces the RFC before serving anything:
- at least one
Contactand anExpiresvalue must be configured; - web URIs must use
https://; - contact values must be URIs — plain email addresses are rejected with a hint to use
mailto:; - values may not contain line breaks, preventing header-style injection through config values;
- extension field names must be valid RFC 5322 field names.
Invalid configuration throws a descriptive SecurityTxtWasInvalid exception rather than serving a malformed file.
Testing
composer phpunit