Stella Product Security Metadata Contract v1
Contract: stella.ops/productSecurityMetadata@v1 Status: Active Sprint: SPRINT_20260430_001 Owner: Stella Ops manufacturer / Router Gateway owner Schema-pinning policy: Pinned per Stella minor release; /.well-known/stella-product-security.json MUST NOT auto-track upstream metadata templates. Adding new top-level fields requires a v2 bump. Adding new currentMinorTrains[] entries is a runtime publication action and not a schema change. Deterministic fixture location: Router integration coverage lives under src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Integration/ProductMetadataEndpointsTests.cs. The endpoint serving this document MUST fail closed if publication is disabled, sealed config is absent, operational preflight is incomplete, the release manifest signature is not verified, or any required manufacturer metadata is missing or still holds a documented TBD-* placeholder. Replay/offline expectations: The served document and any backing schema files MUST be present in offline-kit installations; consumers (CSAF feed indexer, advisory ingest, doc lint) MUST validate without external network access.
Purpose
Defines the JSON shape served by Router at /.well-known/stella-product-security.json before static console fallback or the generic /.well-known Authority proxy. This document is the machine-readable companion to the Stella CVD policy and to the public support/EOL policy.
Audience: Router/Gateway implementers, release engineers who author the signed update manifest, and downstream consumers of Stella’s product-security metadata.
It is consumed by:
- Vulnerability researchers using
security.txt(see../security/security-txt.md). - Downstream advisory aggregators (CSAF mirrors, vendor advisory indexers).
- Stella Ops customers who consume the product CSAF feed for product-security advisories about Stella itself.
- Internal lifecycle dashboards that surface support windows.
Endpoint family and field sources (implementation)
This document is produced by ProductMetadataDocumentProvider (in src/Router/StellaOps.Gateway.WebService/ProductMetadata/) and surfaced by ProductMetadataMiddleware, which intercepts three sibling paths before the routing pipeline:
| Path | Method | Body |
|---|---|---|
/.well-known/stella-product-security.json | GET/HEAD | this contract (stella.ops/productSecurityMetadata@v1) |
/.well-known/security.txt | GET/HEAD | RFC 9116 plaintext (Contact/Encryption/Preferred-Languages/Canonical/Policy/Expires; Expires = GeneratedAtUtc + 1 year) |
/api/v1/product/support-lifecycle | GET/HEAD | stella.ops/productLifecycle@v1 lifecycle document |
The endpoint is unauthenticated — there is no StellaOps scope gate in ProductMetadataMiddleware; access control is “is the deployment configured to publish” (the fail-closed gate), not an OAuth scope. (No entry in StellaOpsScopes applies to this surface.)
Provenance of the served fields:
- Constant in code:
productName(Stella Ops),productSecurityContact.intakeMailbox(security@stella-ops.org),productSecurityContact.preferredLanguages(["en"]),pgpKey.fingerprint(the hardcodedPgpFingerprint),schemaVersion, and thesupportPolicy.defaultWindowYears/eolDeprecationNoticeMonthsliterals (5/12). - Derived from
CanonicalBaseUrl:policyUrl,pgpKey.url,cveFeedUrl,csafFeedUrl. - Configured (sealed options):
lastModified(=GeneratedAtUtc). - Read from the signed release manifest
stella.ops/productUpdateManifest@v1:placedOnMarketAt,madeAvailableInEu,craApplicability, and the singlesupportPolicy.currentMinorTrains[0](manifestsupportPolicy.releaseTrain).
Schema version metadata
| Field | Value |
|---|---|
schemaVersion | stella.ops/productSecurityMetadata@v1 |
schemaPinningPolicy | per-stella-minor-release |
breakingChangesRequire | new vN+1 document plus migration notes |
nonBreakingExtensions | additive optional fields permitted within v1 only when documented in this contract’s revision log |
Any document carrying schemaVersion = stella.ops/productSecurityMetadata@v1 MUST conform exactly to the field set below.
JSON shape and committed planning example
The example below records the approved CRA-Q1 through CRA-Q3 planning defaults for the 2026.9 release train. It is not a live publication artifact. The pgpKey.fingerprint value is the website-derived release-signing lineage fingerprint, not evidence that the expired release@stella-ops.org key is a live security-intake key. The served endpoint must fail closed until a non-expired encryption-capable security-intake key or subkey is published and the configured live fingerprint is verified.
{
"schemaVersion": "stella.ops/productSecurityMetadata@v1",
"productName": "Stella Ops",
"productSecurityContact": {
"intakeMailbox": "security@stella-ops.org",
"policyUrl": "https://stella-ops.org/security/coordinated-vulnerability-disclosure",
"preferredLanguages": ["en"]
},
"pgpKey": {
"url": "https://stella-ops.org/.well-known/stella-product-security-pgp.asc",
"fingerprint": "1A045258307194881527D2FE095C5809C63DBA65"
},
"cveFeedUrl": "https://stella-ops.org/security/advisories/index.json",
"csafFeedUrl": "https://stella-ops.org/security/csaf/feed.json",
"supportPolicy": {
"defaultWindowYears": 5,
"eolDeprecationNoticeMonths": 12,
"currentMinorTrains": [
{
"version": "2026.9",
"releasedAt": "2026-09-01",
"securitySupportUntil": "2031-09-01",
"eolAt": "2031-09-01",
"lts": false
}
]
},
"placedOnMarketAt": null,
"madeAvailableInEu": "2026-09-01",
"craApplicability": "pre-cra",
"lastModified": "2026-04-30T00:00:00Z"
}
Field definitions
Top-level
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion | string | yes | Always stella.ops/productSecurityMetadata@v1. |
productName | string | yes | Always Stella Ops for documents served from a canonical Stella endpoint. |
productSecurityContact | object | yes | Public security contact block. See sub-fields. |
pgpKey | object | yes | PGP key reference for encrypted intake. See sub-fields. |
cveFeedUrl | string (URL) | yes | URL of the CVE-style index for Stella product advisories. |
csafFeedUrl | string (URL) | yes | URL of the CSAF 2.0 advisory feed for Stella product advisories. CSAF version is governed by decisions-log.md §CRA-Q4 and the feed payload contract in stella-product-csaf-advisory-v1.md. |
supportPolicy | object | yes | Support and end-of-life policy block. See sub-fields. |
placedOnMarketAt | string (ISO-8601 date) or null | yes | Date Stella as a product was first placed on the market in the EU under the full-CRA placement anchor. null for pre-full-CRA releases. See ../legal/cra-product-placement-anchor.md. |
madeAvailableInEu | string (ISO-8601 date) | yes | Date the current advertised release was made available to EU users. May be earlier than placedOnMarketAt for pre-full-CRA releases. Read verbatim from the release manifest (required, non-empty). A TBD- value in the manifest fails the endpoint closed (whole-manifest scan), so the served value is always a real date. |
craApplicability | string enum | yes | Expected to be one of: pre-cra, cra-reporting-only (post-2026-09-11, pre-2027-12-11), cra-full (post-2027-12-11 placement). As implemented, the served value is copied verbatim from the release manifest’s craApplicability string (ProductMetadataDocumentProvider.LoadReleaseManifest → GetRequiredString(root, "craApplicability")); the endpoint does NOT validate it against this enum nor compute it from the placement-anchor ADR. The manifest author is responsible for setting the correct value. |
lastModified | string (ISO-8601 timestamp, UTC) | yes | Emitted as the sealed Platform:Product:GeneratedAtUtc configuration value (ProductMetadataDocumentProvider.GetProductSecurityJson → lastModified = readiness.Options.GeneratedAtUtc). It is the configured generation timestamp, NOT the wall-clock time the document was actually served. The endpoint fails closed if this string is not parseable as a date (ProductMetadataOptions.GeneratedAtUtc; default 2026-04-30T00:00:00Z). |
productSecurityContact
| Field | Type | Required | Description |
|---|---|---|---|
intakeMailbox | string (RFC 5322 mailbox) | yes | Canonical security intake. Default: security@stella-ops.org. |
policyUrl | string (URL) | yes | URL of the published CVD policy (../security/coordinated-vulnerability-disclosure.md). |
preferredLanguages | string array | yes | RFC 5646 language tags accepted at intake. Default: ["en"]. |
pgpKey
| Field | Type | Required | Description |
|---|---|---|---|
url | string (URL) | yes | Canonical URL of the intake PGP public key (ASCII-armored). MUST match the Encryption: line in security.txt. |
fingerprint | string | yes | OpenPGP key fingerprint (40-character uppercase hex without spaces). As implemented, this is the compile-time constant ProductMetadataDocumentProvider.PgpFingerprint = "1A045258307194881527D2FE095C5809C63DBA65"; it is emitted verbatim on every published response and is NOT read from the release manifest, configuration, or a live key. This planning value comes from the website-published Stella release-signing key lineage; it is not proof of a live intake key. The KeyFreshnessVerified and KeyFingerprintVerified operational-preflight booleans (ProductMetadataPreflightOptions) gate publication, but the served fingerprint string itself is the constant regardless of which key was verified. Draft/roadmap: the placeholder convention TBD-confirm-after-key-creation and the requirement to emit a verified live fingerprint that may differ from the lineage value are forward-looking; they are not yet enforced or wired (the TBD- placeholder scan applies only to the release manifest — see Validation rules — and the manifest does not carry a PGP fingerprint). |
supportPolicy
| Field | Type | Required | Description |
|---|---|---|---|
defaultWindowYears | integer | yes | Public security-support window from the GA date of each minor train. Default: 5 (per decisions-log.md §CRA-Q2). |
eolDeprecationNoticeMonths | integer | yes | Minimum months of public notice before a release reaches end-of-life. Default: 12. |
currentMinorTrains | array of objects | yes | Catalogue of currently advertised minor trains. See sub-fields. As implemented, the served array always has exactly one element, populated from the release manifest’s supportPolicy.releaseTrain object (ProductMetadataDocumentProvider.GetProductSecurityJson). The endpoint cannot emit multiple trains in v1. The committed planning record is 2026.9 (releasedAt = 2026-09-01, securitySupportUntil/eolAt = 2031-09-01, lts = false). Any TBD- token anywhere in the manifest causes a hard 503 (whole-manifest scan; see Validation rules), so the published array never contains a TBD-release-roadmap placeholder. |
supportPolicy.currentMinorTrains[]
Each element is copied field-for-field from the release manifest’s required supportPolicy.releaseTrain object (GetRequiredString/GetBoolean); all four string fields are required and non-empty, and lts is a required boolean. The endpoint does NOT compute securitySupportUntil/eolAt from releasedAt and does NOT enforce eolAt >= securitySupportUntil — those are conventions the manifest author must honour. A TBD- substring in any of these (via the manifest scan) fails closed, so served values are always concrete.
| Field | Type | Required | Description |
|---|---|---|---|
version | string | yes | Stella minor train identifier (e.g. 2026.9). Served verbatim from manifest releaseTrain.version. |
releasedAt | string (ISO-8601 date) | yes | GA date for the minor train. Served verbatim from manifest releaseTrain.releasedAt. |
securitySupportUntil | string (ISO-8601 date) | yes | End-of-security-support date. Convention (not enforced by the endpoint): releasedAt + defaultWindowYears unless explicitly extended. Served verbatim from the manifest. |
eolAt | string (ISO-8601 date) | yes | End-of-life date (no further patch or commercial support). Convention (not enforced by the endpoint): >= securitySupportUntil. Served verbatim from the manifest. |
lts | boolean | yes | true if this minor train is named LTS for its calendar year. Served verbatim from manifest releaseTrain.lts. The annual LTS rule lives in decisions-log.md §CRA-Q2. |
craApplicability derivation
The intended legal semantics of the value (set by whoever authors the signed release manifest) are:
pre-crawhile the served release was made available before 2026-09-11.cra-reporting-onlyonce the served release was made available on or after 2026-09-11 and before 2027-12-11 (CRA Article 14 reporting applies; full Annex I conformity does not).cra-fullonce the served release was made available on or after 2027-12-11, OR when a pre-2027-12-11 release has undergone a substantial modification on or after 2027-12-11 (seesubstantialModificationOfin the release manifest,../legal/cra-product-placement-anchor.md).
Implementation note (was INCORRECT): the well-known endpoint does not itself compute this value or cross-check it against madeAvailableInEu/placedOnMarketAt. ProductMetadataDocumentProvider.LoadReleaseManifest reads the craApplicability string straight out of the signed product-update-manifest.json (stella.ops/productUpdateManifest@v1) and serves it verbatim, with no enum validation. The manifest is the source-of-truth and is signature-verified (ReleaseManifestSignatureVerified must be true), so “MUST NOT be hand-edited at deploy time” is satisfied by signing the manifest, not by gateway-side computation. The substantialModificationOf field is present in the manifest schema (and in the test fixture) but is not consumed by the gateway provider.
Validation rules
The authoritative fail-closed logic is ProductMetadataDocumentProvider.TryBuildDocuments + LoadReleaseManifest; the HTTP behaviour is ProductMetadataMiddleware.InvokeAsync. The endpoint serves 200 OK only when every check below passes; otherwise it returns 503 Service Unavailable with an empty body and Cache-Control: no-store.
- Canonical base URL MUST be HTTPS. All emitted URLs are built from
CanonicalBaseUrl(defaulthttps://stella-ops.org). A non-HTTPS base URL fails closed (TryNormalizeHttpsBaseUrl). The other emitted URLs (policyUrl,pgpKey.url,cveFeedUrl,csafFeedUrl) are HTTPS only by virtue of that base; they are not individually validated. GeneratedAtUtcMUST parse as a date. IfPlatform:Product:GeneratedAtUtcis not a parseable timestamp, the endpoint fails closed (“metadata generation timestamp is invalid”). (There is no enforced “≤ 24 hours old” freshness check — that previously documented rule is NOT implemented;lastModifiedis simply the configured value.)- Operational preflight MUST be complete.
ProductMetadataPreflightOptions.AllPassedrequires all eight booleans true:MailboxDeliveryVerified,MailboxAccessVerified,KeyFreshnessVerified,KeyFingerprintVerified,PlacementAnchorVerified,SupportLifecycleVerified,RotationVerified,EscalationVerified. Any false → fail closed. (These gate PGP key freshness/fingerprint and mailbox readiness operationally; the servedpgpKey.fingerprintstring is nonetheless always the hardcoded constant — seepgpKey.fingerprint.) - Publication + sealing + signature flags MUST be set. Fail closed when
platform.product.publishMetadatais false,platform.product.sealedConfigis false, orplatform.product.releaseManifestSignatureVerifiedis false. (The gateway trusts these booleans; it does not itself recompute the DSSE/manifest signature.) - Release manifest MUST be present, valid, and matched to the running build. The manifest path (
Platform:Product:ReleaseManifestPath) must exist; the file must parse as JSON;schemaVersionmust equalstella.ops/productUpdateManifest@v1;supportPolicy.lifecycleSchemamust equalstella.ops/productLifecycle@v1; the manifestversionmust equalRunningProductVersion; and whenRunningImageDigestis set, the manifestimages[].digestarray must contain that digest. Any mismatch, missing required field, empty string, or read/IO/auth error fails closed with the corresponding reason (“release manifest is missing”, “…version does not match running product version”, “…does not cover the running image digest”, “release manifest JSON is invalid”, etc.). - No
TBD-placeholder anywhere in the release manifest.RejectPlaceholdersrecursively walks the entire parsed manifest and fails closed (“release manifest contains placeholder metadata”) if ANY string contains the substringTBD-. (Correction: this is a whole-manifest scan, not a check limited topgpKey.fingerprint/placedOnMarketAt/currentMinorTrains. NotepgpKey.fingerprintis not present in the manifest at all, so it is never subject to this scan.) - Failing closed is the binding behaviour from the EU-compliance execution directions §“CRA Sprint Directions” row 002.
- HTTP method gate. Only
GETandHEADare served; other methods return405 Method Not AllowedwithAllow: GET, HEAD(ProductMetadataMiddleware). - Caching / conditional requests. Published
200responses carry anETagandCache-Control. The ETag is"<lowercase-hex SHA-256 of the exact served bytes>"(ComputeETag); it is a normal (strong-form) entity tag, not weak-prefixed.Cache-Controlispublic, max-age=<CacheMaxAgeSeconds>(default 300) orno-cachewhenCacheMaxAgeSeconds <= 0. AnIf-None-Matchvalue equal to the current ETag (or*) returns304 Not Modifiedwith the sameETagandCache-Controland no body. - NOT IMPLEMENTED — feed reachability cross-check. There is no regeneration-time reachability probe of
csafFeedUrl/cveFeedUrl. Both are staticbaseUrl-interpolated strings; the gateway never fetches them, never logs a “regeneration warning”, and has no “serve the previous document” path. (The endpoint is rendered per-request from options + manifest, not regenerated to a cache.) Treat this as Draft/roadmap if a reachability gate is later desired.
Operational preflight
No TBD-* placeholder remains in the committed 2026.9 planning example. The well-known endpoint still MUST fail closed when deployment preflight cannot verify the inputs below. As implemented, preflight is expressed as eight booleans on ProductMetadataPreflightOptions (config prefix Platform:Product:Preflight:*), all of which must be true (AllPassed):
MailboxDeliveryVerifiedandMailboxAccessVerified—security@stella-ops.orgmailbox delivery and access controls.KeyFreshnessVerified— a non-expired encryption-capable security-intake public key atpgpKey.url.KeyFingerprintVerified— the live key fingerprint matching the configured product-security metadata.PlacementAnchorVerified— CRA placement-anchor inputs confirmed.SupportLifecycleVerified— support/EOL lifecycle inputs confirmed.RotationVerified— key/credential rotation readiness confirmed.EscalationVerified— security escalation path confirmed.
These inputs are tracked operationally (verified externally and asserted to the gateway via the boolean flags — the gateway does not itself probe the mailbox or load/inspect the PGP key); they are not open product decisions.
Operational key note: the sibling website repo contains public release-signing key material for Stella Ops Release Signing <release@stella-ops.org> with fingerprint 1A045258307194881527D2FE095C5809C63DBA65, created 2025-07-29 and expired 2025-07-30. The fingerprint is accepted as the website-derived planning/key-lineage value, but live publication requires a new non-expired security-intake key or subkey certified by that lineage during the Stella key ceremony.
Operational retention/access note: the CVD mailbox default is 7-year retention for vulnerability reports and triage records, access limited to the three dedicated product-security roles, and quarterly access review. This does not add a metadata field in v1; it is documented in the CVD policy contract and enforced by operational preflight/audit.
Revision log
| Date (UTC) | Change | Notes |
|---|---|---|
| 2026-05-30 | Doc↔code reconciliation against ProductMetadataDocumentProvider/ProductMetadataOptions/ProductMetadataMiddleware. | Corrected: lastModified = configured GeneratedAtUtc (not actual regeneration time); pgpKey.fingerprint is a hardcoded constant (not manifest/live-key derived, and the TBD- placeholder convention is roadmap-only); craApplicability and currentMinorTrains are read verbatim from the signed release manifest (not computed/enum-validated); the TBD- scan is whole-manifest, not field-scoped. Removed the unimplemented “24h freshness” and “feed reachability cross-check” rules (marked NOT IMPLEMENTED). Added the full fail-closed condition set (HTTPS base URL, generatedAt parse, eight preflight booleans, manifest schema/version/digest matching, JSON validity) and documented the sibling security.txt + support-lifecycle endpoints and unauthenticated access. |
| 2026-04-30 | Recorded CVD mailbox retention/access defaults. | 7-year report retention, three-role access, quarterly access review; no metadata field added. |
| 2026-04-30 | Filled 2026.9 lifecycle and website-derived planning fingerprint. | 2026.9 releases on 2026-09-01 and remains pre-cra; live key freshness is still a deployment preflight requirement. |
| 2026-04-30 | Added operational key lineage note. | Website release-signing key found but expired; endpoint remains fail-closed until a valid security-intake key is published. |
| 2026-04-30 | v1 introduced. | Initial pin; consumes decisions-log.md §CRA-Q1, §CRA-Q2, §CRA-Q3, §CRA-Q4 outputs. |
