security.txt Template and Operator Runbook

Status: Router-owned runtime endpoint; fail-closed until publication and operational preflight pass. Owner: Stella Ops Router Gateway owner. Standard: RFC 9116. Last reviewed: 2026-05-31 (doc↔code reconciliation, second pass).

This document is the authoritative source for the contents of /.well-known/security.txt. src/Router/StellaOps.Gateway.WebService/ serves the runtime endpoint before static console fallback or generic /api/* dispatch (verified: ProductMetadataMiddleware is registered ahead of RouteDispatchMiddleware in Program.cs, and the path is registered as a GatewayRoutes.IsSystemPath so it bypasses the authenticated routing branch). The runtime endpoint regenerates the served file from sealed release metadata; the served file MUST NOT be hand-edited.

The same middleware and document provider (ProductMetadataMiddleware + ProductMetadataDocumentProvider) serve two sibling product-security documents from the identical fail-closed readiness gate:

PathDocumentProvider method
/.well-known/security.txtthis RFC 9116 fileGetSecurityTxt()
/.well-known/stella-product-security.jsonproduct-security metadata (v1 contract)GetProductSecurityJson()
/api/v1/product/support-lifecyclesupport-lifecycle (v1 contract)GetSupportLifecycleJson()

All three paths are anonymous system paths — there is no auth scope guard on them (verified: they are registered in GatewayRoutes.SystemPaths, and the gateway’s AuthorizationMiddleware runs only inside the app.UseWhen(context => !GatewayRoutes.IsSystemPath(...)) branch in Program.cs, so system paths bypass it entirely; there is no security.txt/product-security scope in StellaOpsScopes.cs). This anonymity is required by RFC 9116, which mandates a publicly reachable file. All three respond to GET and HEAD only; any other method returns HTTP 405 with Allow: GET, HEAD. On a HEAD the middleware sets Content-Length, ETag, and Cache-Control but writes no body.

On a successful GET/HEAD the middleware also honours conditional requests: when an If-None-Match header matches the current sha256 ETag (or is *), it returns HTTP 304 Not Modified with the ETag and Cache-Control headers and no body (ProductMetadataMiddleware.RequestHasMatchingETag). The 503 fail-closed response carries Cache-Control: no-store and no ETag.


1. Canonical template

The following block is the template body. Lines beginning with # are comments and are emitted verbatim by the provider. Token replacement is described in §3.

The literal https://stella-ops.org shown below is the default value of the CanonicalBaseUrl option (Platform:Product:CanonicalBaseUrl); the Router substitutes the configured base URL (authority part only, scheme MUST be HTTPS or the endpoint fails closed) into the Encryption:, Canonical:, and Policy: lines at regeneration time. The Contact: mailbox (security@stella-ops.org) and the Preferred-Languages: en value are compile-time constants in ProductMetadataDocumentProvider, not configurable.

# Stella Ops product security contact (RFC 9116)
# Source: docs/security/security-txt.md
# Regeneration runbook: docs/security/security-txt.md Section 4

Contact: mailto:security@stella-ops.org
Encryption: <CanonicalBaseUrl>/.well-known/stella-product-security-pgp.asc
Preferred-Languages: en
Canonical: <CanonicalBaseUrl>/.well-known/security.txt
Policy: <CanonicalBaseUrl>/security/coordinated-vulnerability-disclosure
Expires: {{EXPIRES_ISO8601}}

A frozen rendering with the default base URL is captured in rendered/router-security.txt (see §5).


2. Field obligations

Both /.well-known/security.txt and /.well-known/stella-product-security.json are generated by the same ProductMetadataDocumentProvider from the same option/constant inputs, so the two documents are consistent by construction (the security.txt fields are not read back from the JSON at runtime). The “matches the JSON” framing below is a contract guarantee, not a runtime lookup.

FieldRFC 9116 statusStella binding behaviourSource in provider
Contactrequiredmailto:security@stella-ops.org — matches intakeMailbox in the JSON. Stella publishes a single canonical mailbox; multiple Contact: lines are not used in v1.IntakeMailbox constant
Encryptionrecommended<CanonicalBaseUrl>/.well-known/stella-product-security-pgp.asc — matches pgpKey.url in the JSON.CanonicalBaseUrl option
Preferred-Languagesrecommendeden — matches productSecurityContact.preferredLanguages (RFC 5646 tags). The provider hardcodes the single tag en; the documented join-with-, behaviour for multiple tags is not exercised in v1.constant
Canonicalrecommended<CanonicalBaseUrl>/.well-known/security.txt — the absolute URL where the served file is reachable.CanonicalBaseUrl option
Policyrecommended<CanonicalBaseUrl>/security/coordinated-vulnerability-disclosure — the public URL of the CVD policy.CanonicalBaseUrl option
ExpiresrequiredISO-8601 timestamp in UTC, exactly 12 months in the future from the configured GeneratedAtUtc (generatedAt.AddYears(1), formatted yyyy-MM-ddTHH:mm:ssZ).GeneratedAtUtc option

The URLs embedded in the file (Encryption:, Canonical:, Policy:) are always HTTPS: the endpoint fails closed (HTTP 503) unless CanonicalBaseUrl is an absolute HTTPS URL, and only its authority part is used. Note this is a constraint on the content, not the request transport — the gateway’s HTTPS-redirect explicitly skips system paths (!GatewayRoutes.IsSystemPath(...) in Program.cs), so the well-known endpoint can also be reached over plain HTTP behind a TLS-terminating proxy.

NOT IMPLEMENTED (forward-looking): the Router runtime endpoint emits the file body unsigned — it does not PGP-sign the output, and there is no /.well-known/security.txt.sig route in ProductMetadataMiddleware. The detached-signature obligations described here and in §4 are an operator/break-glass aspiration, not current runtime behaviour. RFC 9116 treats a detached signature as optional, so the unsigned file is still standards-conformant.


3. Token substitution

The well-known endpoint substitutes the following at regeneration time (string-interpolated into the body by ProductMetadataDocumentProvider.GetSecurityTxt()):

SubstitutionSource
{{EXPIRES_ISO8601}}GeneratedAtUtc + 12 months (generatedAt.AddYears(1)), formatted yyyy-MM-ddTHH:mm:ssZ
<CanonicalBaseUrl> (in Encryption:, Canonical:, Policy:)the HTTPS authority of the CanonicalBaseUrl option, trailing slash trimmed

Fail-closed readiness gate

The endpoint returns HTTP 503 with an empty body and Cache-Control: no-store (it never advertises a non-functional contact) unless all of the following hold — verified in ProductMetadataDocumentProvider.TryBuildDocuments():

This is materially broader than “any source value is missing or contains a TBD-* placeholder” — the TBD- rejection is one branch (RejectPlaceholders) of the gate, not the whole gate. The decisions-log.md §CRA-Q1 / stella-product-security-metadata-v1.md §“Validation rules” references remain the planning-input source for these values.


4. Operator regeneration runbook

Implementation note: the Router does not publish a static file via a pipeline. ProductMetadataDocumentProvider regenerates the body in-process on each request from Platform:Product:* options and the signed release manifest, computes a sha256 ETag (lowercase hex, quoted), and serves it with Cache-Control: public, max-age=<CacheMaxAgeSeconds> (Platform:Product:CacheMaxAgeSeconds, default 300s; a value of 0 or less emits Cache-Control: no-cache instead). “Regeneration” in this runbook therefore means rotating the underlying option/manifest inputs (notably GeneratedAtUtc, which drives Expires), not editing a served artifact. The steps below describe the operator process for those inputs.

4.1 Regeneration cadence

4.2 Manual regeneration steps (break-glass only)

  1. Confirm decisions-log.md §CRA-Q1 planning inputs are filled and live preflight passes (mailbox deliverable, non-expired PGP key published, rotation roles assigned, escalation contact named).
  2. Validate /.well-known/stella-product-security.json parses against stella-product-security-metadata-v1.md and contains no TBD-* placeholders in any required field.
  3. Render the template in §1 with the current {{EXPIRES_ISO8601}} value (now + 12 months in UTC).
  4. Validate the rendered file against an RFC 9116 linter (see §5). Reject the rendered output if the linter reports any error.
  5. Capture the regeneration event in the operational audit log: regeneration time, expires time, signing key fingerprint, source-document hashes.

NOT IMPLEMENTED (forward-looking): steps describing generating a detached PGP signature and publishing security.txt.sig atomically are aspirational. The Router endpoint serves an unsigned body and exposes no .sig route (see §2). Until a signing surface exists, do not record signature generation as a completed regeneration step.

4.3 Operator preflight checklist

4.4 Failure modes

To fail the endpoint closed (HTTP 503, empty body, Cache-Control: no-store), flip any of the §3 readiness inputs to its failing state — e.g. set Platform:Product:PublishMetadata=false, set the offending Preflight:* flag to false, or set ReleaseManifestSignatureVerified=false. There is no separate “take the endpoint offline” switch; the gate is the switch.

SymptomRequired action
Expires: within 30 daysRefresh GeneratedAtUtc immediately; this is a SEV-2 endpoint hygiene event.
PGP fingerprint published in stella-product-security.json no longer matches the key at pgpKey.urlFail the endpoint closed via a readiness flag; rotate or republish the key, update the published fingerprint constant, then restore the gate.
Mailbox bounces during preflightSet Preflight:MailboxDeliveryVerified=false (fails the endpoint closed); do not publish a Contact: line that does not deliver.
RFC 9116 linter reports any errorReject the refresh; do not advance the inputs.

5. Verification

The served file MUST validate against an RFC 9116 linter before publication. Suitable linters include:

The repo-local offline reference-style linter is securitytxt-rfc9116-lint.ps1. It is dependency-free, performs no network fetches, and validates rendered artifacts for UTF-8, size, required Contact and Expires fields, registered RFC 9116 field syntax, HTTPS URI fields, RFC 3339 Expires, and the expected canonical URI when supplied.

The current Router-rendered evidence artifact is rendered/router-security.txt. It is rendered from the Router product metadata provider using the sealed published test manifest values exercised by ProductMetadataEndpointsTests.

The completion criterion for SPRINT_20260430_001_DOCS_cra_cvd_policy.md task A1-001-2 is satisfied by running an approved offline/reference RFC 9116 linter against rendered Router output and recording the exact command/result in the sprint evidence. Template review, field presence checks, and Router integration tests are useful prerequisites, but they are not a substitute for linter evidence.


6. Cross-references


7. Revision log

Date (UTC)ChangeNotes
2026-05-31Second doc↔code reconciliation pass against ProductMetadataMiddleware/ProductMetadataDocumentProvider/GatewayRoutes/Program.cs.Documented the conditional-GET 304/If-None-Match behaviour and HEAD body-omission (both verified in source + ProductMetadataEndpointsTests); grounded the no-auth-scope claim in Program.cs UseWhen(!IsSystemPath) + absence from StellaOpsScopes.cs; clarified that the HTTPS guarantee is on the embedded URLs (the system-path transport itself is not force-redirected to HTTPS); noted the CacheMaxAgeSeconds<=0 ⇒ no-cache edge. All prior claims re-verified accurate.
2026-05-30Doc↔code reconciliation against ProductMetadataDocumentProvider/ProductMetadataMiddleware.Corrected §3 token substitution (base-URL substitution + full fail-closed gate), documented sibling endpoints (stella-product-security.json, support-lifecycle) and HTTP/ETag/cache/anonymous-system-path behaviour, and flagged the detached PGP signature (security.txt.sig) as NOT IMPLEMENTED in the Router runtime.
2026-05-01Added repo-local offline linter and rendered Router evidence artifact.securitytxt-rfc9116-lint.ps1 validates rendered/router-security.txt without external network or dependencies.
2026-04-30Template introduced.Initial template + runbook for the served /.well-known/security.txt.