Risk Bundles (Air-Gap)
Audience: operators and release engineers running Stella Ops in offline or air-gapped environments.
Purpose: build and verify deterministic, signed risk-intelligence bundles without network connectivity.
Status: routes, configuration keys, file names and the CLI verify verb re-checked against
src/at commit942a18727c(2026-09-26); the verification behaviour and troubleshooting answers were not re-verified.
Risk bundles package vulnerability-risk intelligence (CISA KEV, FIRST EPSS, OpenSSF OSV) into deterministic, signed archives. They can be verified against their manifest and signatures without network access.
Bundle Structure
A risk bundle is a gzip-compressed tar archive (risk-bundle.tar.gz) with the following structure:
risk-bundle.tar.gz
├── manifests/
│ └── provider-manifest.json # Bundle metadata and provider entries
├── providers/
│ ├── cisa-kev/
│ │ └── snapshot # CISA Known Exploited Vulnerabilities JSON
│ ├── epss/
│ │ └── snapshot # FIRST EPSS scores CSV/JSON
│ └── osv/ # (optional) OpenSSF OSV bulk JSON
│ └── snapshot
└── signatures/
└── provider-manifest.dsse # DSSE envelope for manifest
Provider Manifest
The provider-manifest.json contains bundle metadata and per-provider entries:
{
"version": "1.0.0",
"bundleId": "risk-bundle-20241211-120000",
"createdAt": "2024-12-11T12:00:00Z",
"inputsHash": "sha256:abc123...",
"providers": [
{
"providerId": "cisa-kev",
"digest": "sha256:def456...",
"snapshotDate": "2024-12-11T00:00:00Z",
"optional": false
},
{
"providerId": "epss",
"digest": "sha256:789abc...",
"snapshotDate": "2024-12-11T00:00:00Z",
"optional": true
}
]
}
| Field | Description |
|---|---|
version | Manifest schema version (currently 1.0.0) |
bundleId | Unique identifier for this bundle |
createdAt | ISO-8601 UTC timestamp of bundle creation |
inputsHash | SHA-256 hash of concatenated provider digests (deterministic ordering) |
providers[] | Array of provider entries sorted by providerId |
Provider Entry Fields
| Field | Description |
|---|---|
providerId | Provider identifier (cisa-kev, nvd, osv, ghsa, epss) |
digest | SHA-256 hash of snapshot file (sha256:<hex>) |
snapshotDate | ISO-8601 timestamp of provider data snapshot |
optional | Whether the provider is required for bundle validity |
Provider Catalog
| Provider | Source | Coverage | Refresh | Required |
|---|---|---|---|---|
cisa-kev | CISA Known Exploited Vulnerabilities | Exploited CVEs with KEV flag | Daily | Yes |
epss | FIRST EPSS scores | Exploitation probability per CVE | Daily | No |
nvd | NIST National Vulnerability Database | CVE records | not specified | No |
ghsa | GitHub Security Advisories | OSS advisories | not specified | No |
osv | OpenSSF OSV | OSS advisories with affected ranges | Weekly | No (opt-in) |
Building Risk Bundles
Risk bundles are built by the risk-bundle job on offlinekit-worker (src/OfflineKit/__Libraries/StellaOps.ExportCenter.RiskBundles, StellaOps.ExportCenter.WorkerRuntime). The loop is composed only when RiskBundles:Enabled is true (release bundle: OFFLINEKIT_RISK_BUNDLES_ENABLED, default false); enabled, it requires RiskBundles:Signing:Key and RiskBundles:Storage:RootPath at start and has no unsigned mode.
Jobs are submitted and tracked on offlinekit-web; the whole route group requires the scope export.operator:
| Route | Purpose |
|---|---|
GET /api/offlinekit/v1/risk-bundles/providers | available providers |
POST /api/offlinekit/v1/risk-bundles/jobs | submit a job (selectedProviders, includeOsv, providerOverrides, bundleFileName, allowMissingOptional, allowStaleOptional, metadata) |
GET /api/offlinekit/v1/risk-bundles/jobs, GET …/jobs/{jobId} | job list and status |
POST /api/offlinekit/v1/risk-bundles/jobs/{jobId}/cancel | cancel |
Jobs carry a tenant-scoped idempotency key, so a replayed submission returns the existing job. Artefacts are written under RiskBundles:Storage:RootPath (release bundle: /var/lib/stella/exportcenter/risk-bundles).
Verifying Risk Bundles
Using the CLI
# Basic verification
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz
# With detached signature
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --signature-path ./bundle.sig
# Check Sigstore Rekor transparency log
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --check-rekor
# JSON output for automation
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --json
# Verbose output with warnings
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --verbose
CLI Options
| Option | Description |
|---|---|
--bundle-path, -b | Path to risk bundle file (required) |
--signature-path, -s | Path to detached signature file |
--check-rekor | Verify transparency log entry in Sigstore Rekor |
--json | Output results as JSON |
--tenant | Tenant context for verification |
--verbose | Show detailed output including warnings |
Verification Steps
The verification process performs these checks:
- Archive integrity — bundle is a valid tar.gz archive.
- Structure validation — required files present (
manifests/provider-manifest.json). - Manifest parsing — valid JSON with required fields (
bundleId,version,providers). - Provider hash verification — each provider snapshot matches its declared digest.
- Mandatory provider check —
cisa-kevmust be present and valid. - DSSE signature validation — manifest signature verified (if present).
- Detached signature — bundle archive signature verified (if provided).
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Bundle is valid |
| 1 | Bundle is invalid or verification failed |
| 2 | Input error (missing file, bad arguments) |
JSON Output Format
{
"valid": true,
"bundleId": "risk-bundle-20241211-120000",
"version": "1.0.0",
"providerCount": 2,
"mandatoryProviderFound": true,
"errorCount": 0,
"warningCount": 1,
"errors": [],
"warnings": ["Optional provider not found: osv"]
}
Importing Risk Bundles
At this commit no command or service imports a risk bundle into a sealed estate: there is no import-risk-bundle verb and the predecessor Concelier service is retired. Verify a received bundle with stella risk bundle verify and treat it as reference data until an owning service declares an import contract. Offline content that a service activates travels as an OfflineKit carrier part (Importer).
Signing and Trust
DSSE Manifest Signature
The signatures/provider-manifest.dsse file contains a DSSE envelope over the manifest, signed with the key configured as RiskBundles:Signing:Key / RiskBundles:Signing:KeyId (release-bundle default key id risk-bundle-hmac):
{
"payloadType": "application/stellaops.risk-bundle.provider-manifest+json",
"payload": "<base64-encoded-manifest>",
"signatures": [
{
"keyid": "risk-bundle-hmac",
"sig": "<signature>"
}
]
}
Offline Trust Roots
For air-gapped verification, include the public keys in the bundle:
signatures/
├── provider-manifest.dsse
└── pubkeys/
└── <tenant>.pem
Sigstore/Rekor Integration
When --check-rekor is specified, verification queries the Sigstore Rekor transparency log to confirm the bundle was published to the public ledger. In sealed mode this step is skipped unless an offline Rekor checkpoint is supplied.
Determinism Checklist
Risk bundles are designed for reproducible builds:
- [x] Fixed timestamps for tar entries (
--mtime="@<epoch>") - [x] Sorted file ordering (
--sort=name) - [x] Numeric owner/group (
--owner=0 --group=0 --numeric-owner) - [x] Deterministic gzip compression (
gzip -n) - [x] Providers sorted by
providerIdin manifest - [x] Files sorted lexicographically in bundle
- [x] UTF-8 canonical paths
- [x] ISO-8601 UTC timestamps
Troubleshooting
Q: Hash mismatch after transfer.
A: Binary transfers can corrupt files. Use checksums:
# On source system
sha256sum risk-bundle.tar.gz > checksum.txt
# On target system
sha256sum -c checksum.txt
Q: “Optional provider not found” warning.
A: This is informational. Optional providers (EPSS, OSV) enhance risk analysis but aren’t required.
Related Documentation
- Offline Update Kit: operator guide for the Offline Kit.
- Risk-Bundle Provider Matrix: provider specifications.
- OfflineKit architecture §4: the worker loops and configuration.
