Search by

thoresuenert / tempest-security-txt

thoresuenert

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

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1 2026-08-11 17:01 UTC

This package is auto-updated.

Last update: 2026-08-11 17:02:38 UTC


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 Contact and an Expires value 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