Here’s a practical, copy‑pasteable deployment reference for getting **DSSE/attestations** through your gateway, packaging **air‑gap kits**, and running **canary/blue‑green** with **end‑to‑end tracing** in StellaOps’ **SBOM → VEX** pipeline. --- # 1) Mental model (plain English) * **SBOM→VEX spine**: Sbomer (produces SBOM) → Scanner (finds vulns + reachability) → Concelier (feeds & policy merge) → Excititor/Vexer (VEX verdicts) → Authority (signing/attestations) → Scheduler (rollouts) → Proof Graph (evidence store). * **DSSE** (in‑toto envelope): sign *statements* (SBOMs, scan results, VEX) not just blobs; each hop emits an attestation. * **Gateway**: all outbound (online) and inbound (offline kit ingestion) goes via NGINX/WAF with mTLS and header‑level provenance. * **Determinism**: every artifact has a content hash; every decision has a reproducible *Policy ID* and *Graph Revision ID*. --- # 2) Gateway traversal: DSSE/attestation flow ## 2.1 Required headers & mTLS * **Client auth**: mTLS with client cert mapped to `tenant_id` + `env`. * **Provenance headers** (added by internal callers or sidecar): * `X-Stella-TraceId`, `X-Stella-SpanId` * `X-Stella-Graph-Rev` (hash of dependency graph) * `X-Stella-Policy-Id` (hash of lattice/policy bundle) * `X-Stella-DSSE-SHA256` (hash of DSSE payload) * `X-Stella-Att-Chain` (comma list: sbom,scan,vex) ### NGINX/WAF snippet ```nginx map $ssl_client_s_dn $stella_tenant { default "unknown"; ~CN=(?[^/]+) $cn; } map $http_x_stella_traceid $trace_id { default $request_id; } server { listen 443 ssl; ssl_certificate /etc/nginx/certs/gw.crt; ssl_certificate_key /etc/nginx/certs/gw.key; ssl_client_certificate /etc/nginx/certs/ca.crt; ssl_verify_client on; # Block if required DSSE headers missing on protected routes if ($request_uri ~* ^/(api/(scan|vex|attest))) { if ($http_x_stella_dsse_sha256 = "") { return 400; } if ($http_x_stella_policy_id = "") { return 400; } } proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Request-Id $trace_id; proxy_set_header X-Stella-Tenant $stella_tenant; proxy_pass http://stella_core_upstream; } ``` --- # 3) DSSE signing & bundling (online and air‑gap) ## 3.1 Canonical DSSE statement shapes (JSON) **SBOM attestation** ```json { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "image:repo/app:1.4.2", "digest": {"sha256": ""}}], "predicateType": "https://stella-ops.org/sbom@v1", "predicate": { "format": "CycloneDX@1.6", "bomDigest": "sha256:", "sources": ["sbomer:3.2.0"], "created": "2025-11-28T09:00:00Z" } } ``` **Scan attestation** ```json { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "image:repo/app:1.4.2","digest":{"sha256":""}}], "predicateType": "https://stella-ops.org/scan@v1", "predicate": { "engine": "scanner.webservice:6.0", "reachability": "enabled", "findingsDigest": "sha256:", "graphRev": "" } } ``` **VEX attestation** ```json { "_type":"https://in-toto.io/Statement/v1", "subject":[{"name":"image:repo/app:1.4.2","digest":{"sha256":""}}], "predicateType":"https://stella-ops.org/vex@v1", "predicate":{ "format":"OpenVEX@1.0", "policyId":"", "verdictsDigest":"sha256:", "explainabilityDigest":"sha256:" } } ``` ## 3.2 .NET 10 signing helper (Authority client) ```csharp // StellaOps.Authority.Client var stmt = JsonDocument.Parse(File.ReadAllText(inputPath)); var sig = await authority.SignDsseAsync( statement: stmt, keyRef: "authority:kms:default", annotations: new() { ["graphRev"] = graphRev, ["policyId"] = policyId }); await File.WriteAllTextAsync(outputPath, sig.EnvelopeJson); ``` ## 3.3 Air‑gap “offline kit” layout ``` offline-kit/ manifest.yaml keys/ trust-root.pem # Root/anchor(s) (FIPS/EIDAS/GOST if needed) offline-signer.key # Encrypted PKCS#8 (no plaintext keys in git) artifacts/ image_app_1.4.2.oci.tgz # air‑gapped OCI save sbom.cdx.json scan.findings.json vex.openvex.json attestations/ sbom.dsse.json scan.dsse.json vex.dsse.json indexes/ rekor-mirror/ # optional signed index (CAR/SQLite) ``` **manifest.yaml** ```yaml version: 1 tenant: bulstrad env: prod graph_rev: "" policy_id: "" artifacts: - name: "image:repo/app:1.4.2" digest: "sha256:" files: sbom: "artifacts/sbom.cdx.json" scan: "artifacts/scan.findings.json" vex: "artifacts/vex.openvex.json" attestations: - type: sbom file: "attestations/sbom.dsse.json" - type: scan file: "attestations/scan.dsse.json" - type: vex file: "attestations/vex.dsse.json" crypto: profile: "FIPS" # or EIDAS/GOST/SM pq_mode: false # enable if Dilithium/Falcon required ``` ## 3.4 Building the kit (CI example) ```bash # Save OCI image for air‑gap ctr -n k8s.io images export artifacts/image_app_1.4.2.oci.tgz repo/app:1.4.2 # Produce SBOM, scan, VEX (deterministic order & hashing) sbomer gen repo/app:1.4.2 > artifacts/sbom.cdx.json scanner reach repo/app:1.4.2 --graph-rev "$GRAPH_REV" > artifacts/scan.findings.json vexer make --policy "$POLICY_ID" artifacts/scan.findings.json > artifacts/vex.openvex.json # Sign via Authority stella authority sign-dsse artifacts/sbom.cdx.json > attestations/sbom.dsse.json stella authority sign-dsse artifacts/scan.findings.json > attestations/scan.dsse.json stella authority sign-dsse artifacts/vex.openvex.json > attestations/vex.dsse.json # Final bundle tar -C offline-kit -czf offline-kit.app-1.4.2.tgz . ``` --- # 4) CI/CD patterns (canary, blue‑green) with VEX gating ## 4.1 Policy gate (pipeline step) * **Fail** if: VEX verdict contains any `Affected` with `severity ≥ High` and `reachability=reachable` **AND** no runtime mitigation present. * **Warn** if: `Reachability=unknown` but `compensatingControl=true`. ```yaml # .gitlab-ci.yml / azure-pipelines.yml analog stages: [build, attest, gate, deploy] gate:vex: stage: gate image: stellaops/cli:latest script: - stella vex gate \ --vex attestations/vex.dsse.json \ --policy-id "$POLICY_ID" \ --deny "affected && reachable && severity>=high && !mitigated" ``` ## 4.2 Canary → Blue‑Green rollout (Kubernetes) ```yaml # canary VirtualService (Istio) – start 5% traffic spec: http: - route: - destination: {host: app-green, subset: v1} # stable weight: 95 - destination: {host: app-blue, subset: v2} # canary weight: 5 ``` **Promotion logic** 1. Deploy **blue** with labels `graphRev`, `policyId`, `traceSeed`. 2. Inject sidecar that emits **DSSE header echoes** and **OpenTelemetry** spans. 3. Watch SLOs (p95 latency, error rate) + **Security SLO**: “no new reachable High/Critical since trace start”. 4. If good for N minutes → shift 5→25→50→100; else rollback to green. ## 4.3 Tracing: what to emit * **Trace span attributes**: * `stella.graph_rev`, `stella.policy_id`, `stella.att_chain="sbom,scan,vex"` * `stella.image_digest`, `stella.env`, `stella.tenant` * `stella.proof_ref` (URI into Proof‑of‑Integrity Graph) * **Log/Span event** on gate decisions: * `vex.decision=allow|deny|warn` * `vex.reasons=[CVE‑..., rule:... ]` * `vex.proof_hash=sha256:...` Minimal .NET 10 OpenTelemetry wiring: ```csharp builder.Services.AddOpenTelemetry() .WithTracing(t => t .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddSource("StellaOps.*") .SetResourceBuilder(ResourceBuilder.CreateDefault() .AddService("stella.app") .AddAttributes(new [] { new KeyValuePair("stella.graph_rev", graphRev), new KeyValuePair("stella.policy_id", policyId) })) .AddOtlpExporter()); ``` --- # 5) Air‑gap ingestion (customer side) ## 5.1 Verify, import, and replay ```bash # Verify DSSE envelopes against trust-root stella authority verify offline-kit/attestations/*.dsse.json --trust-root offline-kit/keys/trust-root.pem # Import into local mirrors stella mirror import offline-kit/indexes/rekor-mirror stella registry load artifacts/image_app_1.4.2.oci.tgz # Replay scan & decisions for audit (deterministic) stella replay --manifest offline-kit/manifest.yaml --strict ``` **Expected outputs** * `Proof-Graph` node linking image digest → SBOM → Scan → VEX with signature chain. * A **Receipt** (JSON) recording hashes + tool versions for audit. --- # 6) DSSE/attestation checklist (use in PRs) **Build & Sign** * [ ] SBOM generated (CycloneDX 1.6), hash recorded * [ ] Scan findings (reachability ON), hash recorded * [ ] VEX derived via lattice policy, `policy_id` locked * [ ] All three DSSE‑signed by Authority * [ ] Rekor (or mirror) entry created or queued (if air‑gapped) **Gateway Compliance** * [ ] mTLS client cert validated → tenant/env mapped * [ ] Required DSSE headers present * [ ] TraceId propagated to all services **Determinism** * [ ] Graph Revision ID logged in traces + headers * [ ] Same inputs → identical verdicts on replay **Deployment** * [ ] VEX gate step passes with deny‑rules configured * [ ] Canary 5% with Security SLO watch * [ ] Auto‑promote to blue‑green if SLOs met **Air‑Gap** * [ ] Offline kit contains: SBOM, Scan, VEX, DSSEs, trust root, OCI tar * [ ] Manifest.yaml present with policy_id + graph_rev * [ ] Verify → Import → Replay documented for customer --- # 7) Reference: CI fragments you can drop in today ## 7.1 Make targets ```Makefile GRAPH_REV := $(shell stella graph rev ./) POLICY_ID := $(shell stella policy id ./policies) kit: mkdir -p offline-kit/{artifacts,attestations,keys,indexes} sbomer gen $(IMG) > offline-kit/artifacts/sbom.cdx.json scanner reach $(IMG) --graph-rev $(GRAPH_REV) > offline-kit/artifacts/scan.findings.json vexer make --policy $(POLICY_ID) offline-kit/artifacts/scan.findings.json > offline-kit/artifacts/vex.openvex.json stella authority sign-dsse offline-kit/artifacts/sbom.cdx.json > offline-kit/attestations/sbom.dsse.json stella authority sign-dsse offline-kit/artifacts/scan.findings.json > offline-kit/attestations/scan.dsse.json stella authority sign-dsse offline-kit/artifacts/vex.openvex.json > offline-kit/attestations/vex.dsse.json stella offline manifest --graph-rev $(GRAPH_REV) --policy-id $(POLICY_ID) > offline-kit/manifest.yaml tar -C offline-kit -czf offline-kit.$(VERSION).tgz . ``` ## 7.2 K8s labels/annotations to keep proofs attached ```yaml metadata: labels: stella.graph_rev: "" stella.policy_id: "" annotations: stella.att_chain: "sbom,scan,vex" stella.proof_ref: "proofgraph://images/" ``` --- # 8) Tricky edges & how to handle them * **Huge envelopes (Rekor v2 limits)**: chunk large predicates (scan findings) and include `findingsDigest` in DSSE; store full findings in artifact store, only digest in log. * **“Unknown” reachability**: gate as *warn* but require runtime sensor in canary; promote only if no new reachable High/Critical appears. * **Crypto sovereignty**: select `crypto.profile` per client (FIPS, EIDAS, GOST, SM). For PQ mode, sign dual: `(ECDSA,PQ)` and record both in Receipt. * **Repeatability**: pin tool versions; include `--seed` for any randomized analysis; freeze feeds via Concelier snapshot IDs in the kit. --- # 9) What to ask your team to implement next (small, shippable) 1. **Headers at the gateway** (block missing DSSE fields on protected APIs). 2. **Authority client** in .NET 10 with `SignDsseAsync()` and `VerifyAsync()`. 3. **Make target `kit`** producing the offline bundle with manifest. 4. **VEX gate** step wired into CI with clear deny rules. 5. **Istio canary YAML** + OpenTelemetry attributes emitting `graph_rev`/`policy_id`. 6. **Replay command** that re‑computes decisions and compares hashes (audit). If you want, I can convert this into a ready‑to‑commit `docs/deploy/airgap-playbook.md` plus a `samples/offline-kit/` skeleton and a minimal `.gitlab-ci.yml` in your repo. Understood. I’ll treat this as a full implementation brief you can drop into your repo and hand to: backend devs, DevOps, security, and docs. I’ll structure it as if it lived in: * `docs/deploy/attestations-and-airgap.md` * plus references to code locations (e.g. `src/StellaOps.Authority.Client/` etc.) You can forward this directly to the team. --- ## 0. Scope and objectives ### 0.1 What we are building End-to-end, deterministic, attestable flow: 1. **SBOM → Scanner → VEX** for each image. 2. Each step emits a **DSSE attestation** signed by **Authority**. 3. Attestations are: * Logged online (Rekor v2) or * Bundled in an **offline kit** for air-gapped environments. 4. CI/CD uses these attestations to: * **Gate deployments** (block if risky) * Run **canary → blue-green** with security SLOs. 5. All of this is traceable via: * `graphRev` (Graph Revision ID) * `policyId` (Lattice / policy bundle ID) * **Proof-of-Integrity Graph**. ### 0.2 Non-negotiable invariants * Lattice algorithms run in **scanner.webservice**, not in Concelier or Vexer/Excititor. * Same inputs → same graph → same VEX verdicts → same hashes. * No unsigned SBOM/scan/VEX artifacts are consumed by gates. * Every deployment points to: * A specific image digest, * A specific graph revision, * A specific policy bundle. --- ## 1. Core concepts & identifiers ### 1.1 Graph Revision ID (`graphRev`) **Definition**: `graphRev` is a deterministic SHA-256 hash over the *canonical* dependency graph structure and all feed versions that affected the scan. Implementation (Scanner): * Scanner builds a canonical JSON of the graph: * Sorted node list (by package name + version) * Sorted edges * Feed snapshot IDs (from Concelier) * Tool versions (`scannerVersion`, `sbomerVersion`) * Serialize with: * Sorted keys * No whitespace * Compute `graphRev = SHA256(canonicalJson)` as hex string. Store: * Persist in Scanner DB record: `ScanResult.GraphRev` * Return in Scan API. * Include in: * Scan predicate * VEX predicate * Deployment labels/annotations * DSSE annotations. ### 1.2 Policy ID (`policyId`) **Definition**: deterministic SHA-256 hash over the policy/lattice configuration that produced VEX decisions. Responsibility: * **Concelier** owns policy/lattice bundles and publishes a *policy snapshot*. * Vexer uses the *snapshot* referenced in Scanner’s results, it does not recompute the lattice. Computation (Concelier): * Canonical JSON of: * Rule files (normalized and sorted) * Lattice definitions * Region profile * `policyId = SHA256(canonicalPolicyJson)`. Consumers: * Vexer only reads the pre-evaluated lattice result ID provided by Scanner. * VEX predicate includes `policyId`. * CI gate uses `policyId` to know exactly what policy produced a verdict. --- ## 2. Data contracts (JSON / YAML) ### 2.1 In-toto Statement base All Stella statements are in-toto v1: ```json { "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "image:repo/app:1.4.2", "digest": { "sha256": "" } } ], "predicateType": "https://stella-ops.org/@v1", "predicate": { } } ``` Allowed `predicateType` values: * `https://stella-ops.org/sbom@v1` * `https://stella-ops.org/scan@v1` * `https://stella-ops.org/vex@v1` #### 2.1.1 SBOM predicate ```json { "format": "CycloneDX@1.6", "bomDigest": "sha256:", "sources": [ "sbomer:3.2.0" ], "created": "2025-11-28T09:00:00Z" } ``` SBOM JSON itself is separate, referenced via `bomDigest`. #### 2.1.2 Scan predicate Scanner is the **only** component that runs lattice logic. ```json { "engine": "scanner.webservice:6.0.0", "reachability": "enabled", "findingsDigest": "sha256:", "graphRev": "", "feedSnapshotId": "", "latticeEvalId": "" } ``` * `findingsDigest`: hash of canonical scan findings JSON. * `latticeEvalId`: ID of the internal lattice evaluation record that Vexer will rely on (no recomputation). #### 2.1.3 VEX predicate (OpenVEX) ```json { "format": "OpenVEX@1.0", "policyId": "", "verdictsDigest": "sha256:", "explainabilityDigest": "sha256:", "graphRev": "", "latticeEvalId": "" } ``` * `verdictsDigest`: canonical OpenVEX doc hash. * `explainabilityDigest`: hash of a separate explainability JSON (rationale, rules fired, etc., generated from latticeEval). ### 2.2 DSSE envelope Authority returns a DSSE envelope: ```json { "payloadType": "application/vnd.in-toto+json", "payload": "", "signatures": [ { "keyid": "", "sig": "" } ] } ``` Annotations like `graphRev` & `policyId` live in: * Rekor entry (log metadata), and/or * Authority-side audit record. ### 2.3 Offline kit manifest `offline-kit/manifest.yaml`: ```yaml version: 1 tenant: bulstrad env: prod image: name: "image:repo/app:1.4.2" digest: "sha256:" graph_rev: "" policy_id: "" artifacts: sbom: "artifacts/sbom.cdx.json" scan: "artifacts/scan.findings.json" vex: "artifacts/vex.openvex.json" attestations: sbom: "attestations/sbom.dsse.json" scan: "attestations/scan.dsse.json" vex: "attestations/vex.dsse.json" feeds: concelier_feed_snapshot_id: "" crypto: profile: "FIPS" # or EIDAS/GOST/SM pq_mode: false # Dilithium/Falcon dual-sign when true ``` ### 2.4 Replay receipt A replay run must produce a `receipt.json`: ```json { "schemaVersion": 1, "imageDigest": "sha256:", "graphRev": "", "policyId": "", "sbomDigest": "sha256:", "scanFindingsDigest": "sha256:", "vexDigest": "sha256:", "tools": { "sbomer": "3.2.0", "scanner": "6.0.0", "vexer": "2.1.0", "concelier": "5.4.0" }, "feeds": { "concelierFeedSnapshotId": "" }, "status": "ok" # or "mismatch" } ``` Acceptance: replay is **successful** only if all digests match. --- ## 3. Service responsibilities ### 3.1 Sbomer Code location: `src/StellaOps.Sbomer/` Responsibilities: * Generate CycloneDX SBOM for: * OCI images * Local filesystem / rootfs * Guarantee deterministic output: * Sorted components * Stable IDs * Return: * SBOM JSON * `bomDigest` (SHA-256) * Emit SBOM in: * File store (for offline kit) * Optional API. Required changes: * Add endpoint: `POST /api/sbom/image` * Input: `{ "image": "repo/app:1.4.2" }` * Output: `sbomJson` + `bomDigest`. * Add CLI: `sbomer gen ` → `stdout` SBOM JSON. ### 3.2 Scanner.webservice Code: `src/StellaOps.Scanner.WebService/` Responsibilities: * Take SBOM & feeds snapshot from Concelier. * Build canonical dependency graph. * Run: * Vuln matching * Reachability analysis * Lattice evaluation (only here). * Issue: * Scan findings JSON * `findingsDigest` * `graphRev` * `latticeEvalId` * `feedSnapshotId` * Call Authority to sign DSSE scan statement. API: * `POST /api/scan/image` * Input: `image`, optional `sbom` or `sbomRef`. * Output: * `scanFindingsJson` * `scanStatement` (in-toto) * `scanDsseEnvelope`. Persistence: * Store all canonical inputs required for deterministic replay: * SBOM digest * Feeds snapshot ID * Tool versions * Graph structure * Lattice evaluation payload. ### 3.3 Concelier (Feedser successor) Code: `src/StellaOps.Concelier/` Responsibilities: * Maintain feed snapshots: * CVE feeds * Vendor advisories * Configuration. * Create **feed snapshot IDs** that are: * Immutable * Hash-based. * Publish: * `feedSnapshotId` → used by Scanner. * `policyId` → hash of lattice/policy config. Important: Concelier **does not** run lattice; it only provides configuration. Scanner consumes configs, executes lattice, and stores resulting `latticeEvalId`. ### 3.4 Vexer / Excititor Code: `src/StellaOps.Vexer/` (and/or `Excititor`) Responsibilities: * Convert Scanner’s canonical findings & lattice evaluation into **OpenVEX**. * Never re-run lattice; only: * Read `latticeEvalId` * Render it into OpenVEX + explainability JSON. Inputs: * `scanFindingsJson` or reference * `graphRev` * `policyId` * `latticeEvalId`. Outputs: * OpenVEX JSON (deterministic ordering). * Explainability JSON. * VEX in-toto statement. * DSSE envelope via Authority. ### 3.5 Authority Code: `src/StellaOps.Authority/` and `src/StellaOps.Authority.Client/` Responsibilities: * Manage signing keys (FIPS/EIDAS/GOST/SM; optional PQ). * Provide HTTP API: * `POST /api/dsse/sign` * Input: in-toto statement JSON + `keyRef` + annotations. * Output: DSSE envelope JSON. * `POST /api/dsse/verify` * Input: DSSE envelope + trust bundle. * Output: `valid`, `keyId`, `alg`, `signedAt`. * Write log records suitable for: * Rekor v2 online * Offline Rekor mirror. Client library (C#): * `IAuthorityClient.SignDsseAsync(JsonDocument stmt, string keyRef, IDictionary? annotations);` * `IAuthorityClient.VerifyDsseAsync(JsonDocument envelope, TrustBundle bundle);` ### 3.6 Gateway (NGINX / WAF) Responsibility: enforce headers, propagate trace IDs, authenticate tenants via mTLS. Key headers: * `X-Stella-TraceId` * `X-Stella-SpanId` * `X-Stella-Graph-Rev` * `X-Stella-Policy-Id` * `X-Stella-DSSE-SHA256` * `X-Stella-Att-Chain`. NGINX sketch: ```nginx map $ssl_client_s_dn $stella_tenant { default "unknown"; ~CN=(?[^/]+) $cn; } map $http_x_stella_traceid $trace_id { default $request_id; } server { listen 443 ssl; ssl_certificate /etc/nginx/certs/gw.crt; ssl_certificate_key /etc/nginx/certs/gw.key; ssl_client_certificate /etc/nginx/certs/ca.crt; ssl_verify_client on; if ($request_uri ~* ^/(api/(scan|vex|attest))) { if ($http_x_stella_dsse_sha256 = "") { return 400; } if ($http_x_stella_policy_id = "") { return 400; } } proxy_set_header X-Request-Id $trace_id; proxy_set_header X-Stella-Tenant $stella_tenant; proxy_pass http://stella_core_upstream; } ``` ### 3.7 Proof-of-Integrity Graph Code: `src/StellaOps.ProofGraph/` Responsibilities: * Store edges: * `ImageDigest → SBOM(bomDigest)` * `ImageDigest → Scan(findingsDigest, graphRev, latticeEvalId)` * `ImageDigest → VEX(vexDigest, policyId)` * `Attestation → DSSE(keyId, alg, time)` * `RekorEntry → Envelope`. * Provide lookup by: * Image digest * GraphRev * PolicyId. Expose API: * `GET /api/proof/image/{digest}` * `GET /api/proof/graph/{graphRev}` --- ## 4. CLI and .NET integration ### 4.1 CLI tools (stella) Location: `src/Tools/Stella.Cli/` Subcommands (to implement): 1. `stella authority sign-dsse output.dsse.json` * Flags: `--key-ref`, `--annot graphRev=...`, `--annot policyId=...`. 2. `stella offline manifest --graph-rev --policy-id --image > offline-kit/manifest.yaml` 3. `stella vex gate` * Args: * `--vex` (path to VEX DSSE or OpenVEX) * `--policy-id` * `--deny` expression. 4. `stella offline verify` * Verifies DSSE against trust root. 5. `stella offline replay` * Recompute decisions from offline kit, emit `receipt.json`. ### 4.2 Authority client example (.NET 10) ```csharp public sealed class AuthorityClient : IAuthorityClient { private readonly HttpClient _http; public AuthorityClient(HttpClient http) => _http = http; public async Task SignDsseAsync( JsonDocument statement, string keyRef, IDictionary? annotations = null, CancellationToken ct = default) { var payload = new { keyRef, statement = JsonSerializer.Deserialize(statement.RootElement.GetRawText()), annotations }; using var resp = await _http.PostAsJsonAsync("/api/dsse/sign", payload, ct); resp.EnsureSuccessStatusCode(); await using var stream = await resp.Content.ReadAsStreamAsync(ct); return await JsonDocument.ParseAsync(stream, cancellationToken: ct); } } ``` --- ## 5. CI/CD pipeline pattern Assume GitLab, adapt easily. ### 5.1 Environment variables Define: * `IMAGE` – e.g. `repo/app:1.4.2` * `OCI_DIGEST` – from registry. * `GRAPH_REV` – from Scanner or `stella graph rev`. * `POLICY_ID` – from Concelier. ### 5.2 Example pipeline ```yaml stages: - build - attest - gate - deploy build: stage: build script: - docker build -t "$IMAGE" . - OCI_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE" | cut -d'@' -f2) - echo "OCI_DIGEST=$OCI_DIGEST" >> build.env artifacts: reports: dotenv: build.env attest: stage: attest image: stellaops/cli:latest dependencies: [build] script: - sbomer gen "$IMAGE" > offline-kit/artifacts/sbom.cdx.json - scanner reach "$IMAGE" --graph-rev "$GRAPH_REV" > offline-kit/artifacts/scan.findings.json - vexer make --policy "$POLICY_ID" offline-kit/artifacts/scan.findings.json > offline-kit/artifacts/vex.openvex.json - stella authority sign-dsse offline-kit/artifacts/sbom.cdx.json > offline-kit/attestations/sbom.dsse.json - stella authority sign-dsse offline-kit/artifacts/scan.findings.json > offline-kit/attestations/scan.dsse.json - stella authority sign-dsse offline-kit/artifacts/vex.openvex.json > offline-kit/attestations/vex.dsse.json - stella offline manifest \ --graph-rev "$GRAPH_REV" \ --policy-id "$POLICY_ID" \ --image "$OCI_DIGEST" \ > offline-kit/manifest.yaml artifacts: paths: - offline-kit/ gate:vex: stage: gate image: stellaops/cli:latest dependencies: [attest] script: - stella vex gate \ --vex offline-kit/attestations/vex.dsse.json \ --policy-id "$POLICY_ID" \ --deny "affected && reachable && severity>=high && !mitigated" deploy: stage: deploy script: - # Helm / kubectl apply using canary config when: on_success ``` --- ## 6. Kubernetes: canary + blue-green ### 6.1 Labels & annotations Deployment template: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: app-blue labels: app: app track: blue stella.graph_rev: "" stella.policy_id: "" annotations: stella.att_chain: "sbom,scan,vex" stella.proof_ref: "proofgraph://images/" spec: template: metadata: labels: app: app track: blue annotations: sidecar.opentelemetry.io/inject: "true" spec: containers: - name: app image: repo/app@sha256: env: - name: STELLA_GRAPH_REV valueFrom: fieldRef: fieldPath: metadata.labels['stella.graph_rev'] - name: STELLA_POLICY_ID valueFrom: fieldRef: fieldPath: metadata.labels['stella.policy_id'] ``` ### 6.2 Istio VirtualService (canary 5%) ```yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: app spec: hosts: - app.example.local http: - route: - destination: host: app subset: blue weight: 5 - destination: host: app subset: green weight: 95 ``` Promotion logic (Scheduler or CD script): * Shift 5 → 25 → 50 → 100 if: * Error rate below threshold. * p95 latency OK. * No new reachable High/Critical vulns appear in canary telemetry (Security SLO). --- ## 7. Air-gap flow ### 7.1 Producer side (online) Steps: 1. Build image + get digest. 2. Generate SBOM, scan, VEX. 3. Sign all three via Authority (DSSE). 4. Generate `manifest.yaml`. 5. Optionally: * Export container: `ctr images export artifacts/image_app_1.4.2.oci.tgz repo/app:1.4.2`. 6. Ship `offline-kit.*.tgz` through approved channel. Directory shape (required): ```text offline-kit/ manifest.yaml artifacts/ sbom.cdx.json scan.findings.json vex.openvex.json image_app_1.4.2.oci.tgz # optional but recommended attestations/ sbom.dsse.json scan.dsse.json vex.dsse.json keys/ trust-root.pem # root certs / public keys only indexes/ rekor-mirror/ # optional (CAR / SQLite, etc.) ``` ### 7.2 Consumer side (air-gapped environment) Commands (CLI to implement): ```bash # 1) Verify DSSE signatures stella offline verify \ --trust-root offline-kit/keys/trust-root.pem \ offline-kit/attestations/*.dsse.json # 2) Import Rekor mirror (if present) stella mirror import offline-kit/indexes/rekor-mirror # 3) Load image stella registry load offline-kit/artifacts/image_app_1.4.2.oci.tgz # 4) Replay for audit stella offline replay \ --manifest offline-kit/manifest.yaml \ --output receipt.json ``` Acceptance for customers: * Replay must show `status = "ok"`. * Any hash mismatch must be treated as: * Either misconfiguration or * Tampering → block. --- ## 8. Tracing & observability ### 8.1 OpenTelemetry attributes For all services in the chain, attach: * `stella.graph_rev` * `stella.policy_id` * `stella.image_digest` * `stella.tenant` * `stella.env` * `stella.att_chain` (sbom,scan,vex) * `stella.proof_ref` .NET 10 setup example: ```csharp builder.Services.AddOpenTelemetry() .WithTracing(t => t.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .SetResourceBuilder( ResourceBuilder.CreateDefault() .AddService("stella.app") .AddAttributes(new[] { new KeyValuePair("stella.graph_rev", graphRev), new KeyValuePair("stella.policy_id", policyId) }) ) .AddOtlpExporter()); ``` ### 8.2 Security gate span events On VEX gate evaluation: * Add span event `vex.decision` with attributes: * `vex.decision = allow|deny|warn` * `vex.reasons = [...CVE ids / rules...]` * `vex.proof_hash = sha256:` --- ## 9. Work breakdown by area You can assign roughly as follows. ### 9.1 Backend (.NET) team 1. **Authority** * Implement `/api/dsse/sign` and `/api/dsse/verify`. * Implement `StellaOps.Authority.Client`. * Integrate with HSM / KMS if needed. 2. **Scanner** * Implement canonical graph normalization and `graphRev`. * Integrate feed snapshots from Concelier. * Implement lattice evaluation in Scanner only. * Produce scan in-toto statement & call Authority. 3. **Vexer** * Implement OpenVEX rendering from `latticeEvalId`. * Implement explainability JSON generation. * Produce VEX in-toto statement & call Authority. 4. **Sbomer** * Ensure deterministic SBOM generation. * Expose `bomDigest`. * Optionally, direct Authority integration for SBOM DSSE. 5. **ProofGraph** * Implement storage model for edges & nodes. * Expose lookup APIs by image, graphRev, policyId. 6. **CLI** * Implement `stella` subcommands as above. ### 9.2 DevOps / Platform * Deploy Authority with HSM/KMS. * Configure NGINX/WAF with required headers & mTLS. * Build GitLab (or other) pipeline: * Build → Attest → Gate → Deploy. * Implement canary → blue-green rollout strategy: * Istio or NGINX Ingress routing. * Standardize K8s labels/annotations for Stella metadata. ### 9.3 Documentation team Produce: 1. **“Attestation & Air-Gap Overview”** for clients: * Non-technical diagrams of SBOM→Scan→VEX→Deploy. * Explanation of DSSE & trust roots. * Replay and audit story. 2. **“Implementing Stella Gates in CI/CD”**: * Examples per CI (GitLab, GitHub Actions, Azure). * VEX gate policy examples. 3. **“Offline Kit & Air-Gap Runbook”**: * Step-by-step producer & consumer instructions. * Screenshots or CLI examples. * FAQ (mismatch handling, lost keys, etc.). --- ## 10. Acceptance criteria (for you to sign off) For each image deployed: 1. There exists an **SBOM DSSE**, **Scan DSSE**, and **VEX DSSE**. 2. All three can be verified against configured trust roots. 3. A **ProofGraph query** by image digest returns: * SBOM, Scan, VEX, their hashes, and key IDs. 4. A **replay** of the offline kit with the current tools & feeds: * Either reproduces the same verdicts (`status=ok`) or clearly reports mismatches. 5. In CI: * Pipelines fail when VEX gate’s deny expression is satisfied. 6. In production: * Every pod for that image has: * `stella.graph_rev` and `stella.policy_id` labels. * Can be traced back from logs / traces to the same graph & policy. If you want, I can next convert this into concrete repository files (e.g. ready-to-commit `docs/*.md` + starter C# interfaces and CLI skeletons) matching your exact folder structure.