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:
| Path | Document | Provider method |
|---|---|---|
/.well-known/security.txt | this RFC 9116 file | GetSecurityTxt() |
/.well-known/stella-product-security.json | product-security metadata (v1 contract) | GetProductSecurityJson() |
/api/v1/product/support-lifecycle | support-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.
| Field | RFC 9116 status | Stella binding behaviour | Source in provider |
|---|---|---|---|
Contact | required | mailto: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 |
Encryption | recommended | <CanonicalBaseUrl>/.well-known/stella-product-security-pgp.asc — matches pgpKey.url in the JSON. | CanonicalBaseUrl option |
Preferred-Languages | recommended | en — 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 |
Canonical | recommended | <CanonicalBaseUrl>/.well-known/security.txt — the absolute URL where the served file is reachable. | CanonicalBaseUrl option |
Policy | recommended | <CanonicalBaseUrl>/security/coordinated-vulnerability-disclosure — the public URL of the CVD policy. | CanonicalBaseUrl option |
Expires | required | ISO-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.sigroute inProductMetadataMiddleware. 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()):
| Substitution | Source |
|---|---|
{{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():
Platform:Product:PublishMetadataistrue.Platform:Product:SealedConfigistrue.- All eight operator preflight flags pass (
Preflight.AllPassed):MailboxDeliveryVerified,MailboxAccessVerified,KeyFreshnessVerified,KeyFingerprintVerified,PlacementAnchorVerified,SupportLifecycleVerified,RotationVerified,EscalationVerified. Platform:Product:ReleaseManifestSignatureVerifiedistrue.GeneratedAtUtcparses as a valid timestamp.CanonicalBaseUrlis an absolute HTTPS URL.ReleaseManifestPathis set and the file exists, parses as JSON, declaresschemaVersion = stella.ops/productUpdateManifest@v1, contains no string anywhere that includes the substringTBD-, has aversionequal to the running product version, and (whenRunningImageDigestis set) lists that digest underimages[].digest. The manifest’ssupportPolicy.lifecycleSchemamust bestella.ops/productLifecycle@v1.
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.
ProductMetadataDocumentProviderregenerates the body in-process on each request fromPlatform:Product:*options and the signed release manifest, computes a sha256ETag(lowercase hex, quoted), and serves it withCache-Control: public, max-age=<CacheMaxAgeSeconds>(Platform:Product:CacheMaxAgeSeconds, default 300s; a value of0or less emitsCache-Control: no-cacheinstead). “Regeneration” in this runbook therefore means rotating the underlying option/manifest inputs (notablyGeneratedAtUtc, which drivesExpires), not editing a served artifact. The steps below describe the operator process for those inputs.
4.1 Regeneration cadence
- The served
Expires:isGeneratedAtUtc+ 12 months. Refresh cadence is at least every 11 months to leave a 30-day safety buffer before expiry. - Refresh also fires on any change to the underlying CVD policy contract (
product-cvd-policy-v1.md) or product-security metadata contract (stella-product-security-metadata-v1.md). - Refresh of the inputs is an operator/deployment action; manual intervention is reserved for break-glass scenarios.
4.2 Manual regeneration steps (break-glass only)
- 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). - Validate
/.well-known/stella-product-security.jsonparses againststella-product-security-metadata-v1.mdand contains noTBD-*placeholders in any required field. - Render the template in §1 with the current
{{EXPIRES_ISO8601}}value (now + 12 months in UTC). - Validate the rendered file against an RFC 9116 linter (see §5). Reject the rendered output if the linter reports any error.
- 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.sigatomically are aspirational. The Router endpoint serves an unsigned body and exposes no.sigroute (see §2). Until a signing surface exists, do not record signature generation as a completed regeneration step.
4.3 Operator preflight checklist
- [ ]
intakeMailboxis deliverable (test send + receive within the last 30 days). - [ ] PGP key referenced by
Encryption:URL parses, matches the published fingerprint, and is not within 90 days of its own expiry. - [ ]
Policy:URL resolves to a 200 response carrying the published CVD policy. - [ ]
Canonical:URL is the actual served URL. - [ ]
Expires:is at least 30 days and at most 12 months in the future. - [ ] RFC 9116 linter (§5) passes.
- [ ] (forward-looking — NOT IMPLEMENTED) PGP signature
security.txt.sigverifies against the published key. The Router serves no.sigartifact today; this check applies only once a signing surface exists.
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.
| Symptom | Required action |
|---|---|
Expires: within 30 days | Refresh 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.url | Fail the endpoint closed via a readiness flag; rotate or republish the key, update the published fingerprint constant, then restore the gate. |
| Mailbox bounces during preflight | Set Preflight:MailboxDeliveryVerified=false (fails the endpoint closed); do not publish a Contact: line that does not deliver. |
| RFC 9116 linter reports any error | Reject the refresh; do not advance the inputs. |
5. Verification
The served file MUST validate against an RFC 9116 linter before publication. Suitable linters include:
- An offline parser bundled with the well-known endpoint sprint (preferred for offline-kit determinism).
- Any RFC 9116 reference parser available to the deployment pipeline.
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
- CVD policy:
coordinated-vulnerability-disclosure.md. - CVD policy contract:
../contracts/product-cvd-policy-v1.md. - Well-known JSON contract:
../contracts/stella-product-security-metadata-v1.md. - Decisions log:
../europe/decisions-log.md§CRA-Q1.
7. Revision log
| Date (UTC) | Change | Notes |
|---|---|---|
| 2026-05-31 | Second 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-30 | Doc↔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-01 | Added 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-30 | Template introduced. | Initial template + runbook for the served /.well-known/security.txt. |
