Files
git.stella-ops.org/docs/product-advisories/25-Nov-2025 - Air‑gap deployment playbook for StellaOps.md
master b3656e5cb7
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
update advisories
2025-11-29 01:32:00 +02:00

33 KiB
Raw Blame History

Heres a practical, copypasteable deployment reference for getting DSSE/attestations through your gateway, packaging airgap kits, and running canary/bluegreen with endtoend 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 (intoto 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 headerlevel 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

map $ssl_client_s_dn $stella_tenant { default "unknown"; ~CN=(?<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 airgap)

3.1 Canonical DSSE statement shapes (JSON)

SBOM attestation

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{"name": "image:repo/app:1.4.2", "digest": {"sha256": "<OCI_DIGEST>"}}],
  "predicateType": "https://stella-ops.org/sbom@v1",
  "predicate": {
    "format": "CycloneDX@1.6",
    "bomDigest": "sha256:<BOM_SHA>",
    "sources": ["sbomer:3.2.0"],
    "created": "2025-11-28T09:00:00Z"
  }
}

Scan attestation

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{"name": "image:repo/app:1.4.2","digest":{"sha256":"<OCI_DIGEST>"}}],
  "predicateType": "https://stella-ops.org/scan@v1",
  "predicate": {
    "engine": "scanner.webservice:6.0",
    "reachability": "enabled",
    "findingsDigest": "sha256:<FINDINGS_SHA>",
    "graphRev": "<GRAPH_REV>"
  }
}

VEX attestation

{
  "_type":"https://in-toto.io/Statement/v1",
  "subject":[{"name":"image:repo/app:1.4.2","digest":{"sha256":"<OCI_DIGEST>"}}],
  "predicateType":"https://stella-ops.org/vex@v1",
  "predicate":{
    "format":"OpenVEX@1.0",
    "policyId":"<POLICY_ID>",
    "verdictsDigest":"sha256:<VEX_SHA>",
    "explainabilityDigest":"sha256:<XPL_SHA>"
  }
}

3.2 .NET 10 signing helper (Authority client)

// 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 Airgap “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        # airgapped 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

version: 1
tenant: bulstrad
env: prod
graph_rev: "<GRAPH_REV>"
policy_id: "<POLICY_ID>"
artifacts:
  - name: "image:repo/app:1.4.2"
    digest: "sha256:<OCI_DIGEST>"
    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)

# Save OCI image for airgap
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, bluegreen) 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.
# .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 → BlueGreen rollout (Kubernetes)

# 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 ProofofIntegrity 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:

builder.Services.AddOpenTelemetry()
  .WithTracing(t => t
    .AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation()
    .AddSource("StellaOps.*")
    .SetResourceBuilder(ResourceBuilder.CreateDefault()
      .AddService("stella.app")
      .AddAttributes(new []
      {
        new KeyValuePair<string, object>("stella.graph_rev", graphRev),
        new KeyValuePair<string, object>("stella.policy_id", policyId)
      }))
    .AddOtlpExporter());

5) Airgap ingestion (customer side)

5.1 Verify, import, and replay

# 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 DSSEsigned by Authority
  • Rekor (or mirror) entry created or queued (if airgapped)

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 denyrules configured
  • Canary 5% with Security SLO watch
  • Autopromote to bluegreen if SLOs met

AirGap

  • 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

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

metadata:
  labels:
    stella.graph_rev: "<GRAPH_REV>"
    stella.policy_id: "<POLICY_ID>"
  annotations:
    stella.att_chain: "sbom,scan,vex"
    stella.proof_ref: "proofgraph://images/<OCI_DIGEST>"

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 recomputes decisions and compares hashes (audit).

If you want, I can convert this into a readytocommit docs/deploy/airgap-playbook.md plus a samples/offline-kit/ skeleton and a minimal .gitlab-ci.yml in your repo. Understood. Ill treat this as a full implementation brief you can drop into your repo and hand to: backend devs, DevOps, security, and docs.

Ill 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 Scanners 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:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "image:repo/app:1.4.2",
      "digest": { "sha256": "<OCI_DIGEST>" }
    }
  ],
  "predicateType": "https://stella-ops.org/<kind>@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

{
  "format": "CycloneDX@1.6",
  "bomDigest": "sha256:<BOM_SHA>",
  "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.

{
  "engine": "scanner.webservice:6.0.0",
  "reachability": "enabled",
  "findingsDigest": "sha256:<FINDINGS_SHA>",
  "graphRev": "<GRAPH_REV>",
  "feedSnapshotId": "<CONCELIER_FEED_SNAPSHOT_ID>",
  "latticeEvalId": "<LATTICE_EVAL_ID>"
}
  • 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)

{
  "format": "OpenVEX@1.0",
  "policyId": "<POLICY_ID>",
  "verdictsDigest": "sha256:<VEX_SHA>",
  "explainabilityDigest": "sha256:<XPL_SHA>",
  "graphRev": "<GRAPH_REV>",
  "latticeEvalId": "<LATTICE_EVAL_ID>"
}
  • 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:

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "<base64url(in-toto-statement-json)>",
  "signatures": [
    {
      "keyid": "<KEY_ID>",
      "sig": "<base64url(signature)>"
    }
  ]
}

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:

version: 1
tenant: bulstrad
env: prod

image:
  name: "image:repo/app:1.4.2"
  digest: "sha256:<OCI_DIGEST>"

graph_rev: "<GRAPH_REV>"
policy_id: "<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: "<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:

{
  "schemaVersion": 1,
  "imageDigest": "sha256:<OCI_DIGEST>",
  "graphRev": "<GRAPH_REV>",
  "policyId": "<POLICY_ID>",
  "sbomDigest": "sha256:<BOM_SHA>",
  "scanFindingsDigest": "sha256:<FINDINGS_SHA>",
  "vexDigest": "sha256:<VEX_SHA>",
  "tools": {
    "sbomer": "3.2.0",
    "scanner": "6.0.0",
    "vexer": "2.1.0",
    "concelier": "5.4.0"
  },
  "feeds": {
    "concelierFeedSnapshotId": "<CONCELIER_FEED_SNAPSHOT_ID>"
  },
  "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 <image>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 Scanners 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<string, string>? 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:

map $ssl_client_s_dn $stella_tenant {
  default "unknown";
  ~CN=(?<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 <input.json >output.dsse.json

    • Flags: --key-ref, --annot graphRev=..., --annot policyId=....
  2. stella offline manifest --graph-rev <rev> --policy-id <id> --image <digest> > 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)

public sealed class AuthorityClient : IAuthorityClient
{
    private readonly HttpClient _http;

    public AuthorityClient(HttpClient http) => _http = http;

    public async Task<JsonDocument> SignDsseAsync(
        JsonDocument statement,
        string keyRef,
        IDictionary<string, string>? annotations = null,
        CancellationToken ct = default)
    {
        var payload = new
        {
            keyRef,
            statement = JsonSerializer.Deserialize<object>(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

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:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-blue
  labels:
    app: app
    track: blue
    stella.graph_rev: "<GRAPH_REV>"
    stella.policy_id: "<POLICY_ID>"
  annotations:
    stella.att_chain: "sbom,scan,vex"
    stella.proof_ref: "proofgraph://images/<OCI_DIGEST>"
spec:
  template:
    metadata:
      labels:
        app: app
        track: blue
      annotations:
        sidecar.opentelemetry.io/inject: "true"
    spec:
      containers:
        - name: app
          image: repo/app@sha256:<OCI_DIGEST>
          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%)

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):

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):

# 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:

builder.Services.AddOpenTelemetry()
    .WithTracing(t =>
        t.AddAspNetCoreInstrumentation()
         .AddHttpClientInstrumentation()
         .SetResourceBuilder(
             ResourceBuilder.CreateDefault()
                 .AddService("stella.app")
                 .AddAttributes(new[]
                 {
                     new KeyValuePair<string, object>("stella.graph_rev", graphRev),
                     new KeyValuePair<string, object>("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:<hash>

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 gates 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.