-
Notifications
You must be signed in to change notification settings - Fork 2
docs: adr-016 added #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlexHardAtWork
wants to merge
4
commits into
main
Choose a base branch
from
581-airgapped-catalog
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
docs: adr-016 added #765
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
26c0c11
docs: adr-016 added
AlexHardAtWork 10d23f3
docs(adr-016): fixed by review comments
AlexHardAtWork 4ea0d3f
docs(adr-016): signing model corrected
AlexHardAtWork 309e1f9
docs(adr-016): by-value transfer + ref rewriting; air-gap operator in…
AlexHardAtWork File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
343 changes: 343 additions & 0 deletions
343
docs/developer-guide/adrs/016-airgapped-catalog-chaining.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,343 @@ | ||
| --- | ||
| status: proposed | ||
| date: 2026-08-19 | ||
| --- | ||
|
|
||
| # Air-Gapped Catalog Chaining via Offline OCM Transport | ||
|
|
||
| > **Decision at a glance.** For a *fully* air-gapped destination Solar chains its | ||
| > catalog **over physical media, not a network**. The transport layer from | ||
| > [ADR 013](013-catalog-chaining.md) — ARC, an online OCI diode — is replaced by a | ||
| > **self-contained, signed OCM CTF archive** carried across the boundary and imported | ||
| > into the destination registry. **Everything downstream of that registry is | ||
| > unchanged from ADR 013**: Solar Discovery scans the destination registry and builds | ||
| > the catalog. The primary pattern **ships the full catalog** so Solar runs *inside* | ||
| > the air-gap and operators select in-domain; a lighter *render-then-transport* | ||
| > pattern is also supported. | ||
|
|
||
| ## Context and Problem Statement | ||
|
|
||
| [ADR 013 — Solar Catalog Chaining via ARC](013-catalog-chaining.md) describes chaining | ||
| a source Solar catalog to one or more destination Solar instances across a security | ||
| boundary. It considered only boundaries that are **connected, at least temporarily**, | ||
| and explicitly deferred the disconnected case ("How we handle catalog chaining between | ||
| air-gapped environment or if at all"). This ADR (spike #581) answers that. | ||
|
|
||
| [ADR 013's decision](013-catalog-chaining.md#decision-outcome) has two independent halves: | ||
|
|
||
| 1. **Pulling OCM packages source → destination** — via ARC, an *online* OCI diode that | ||
| creates Orders which pull artifacts across the boundary. | ||
| 2. **Building the destination catalog** — [Option C: Registry Scan by Solar | ||
| Discovery](013-catalog-chaining.md#option-c-registry-scan-by-solar-discovery): Solar | ||
| Discovery scans the *destination registry* and turns every OCM package into a | ||
| Component / ComponentVersion. | ||
|
|
||
| In a fully air-gapped environment **half 1 is impossible** — there is no network path | ||
| for ARC to pull over. But **half 2 does not care how packages arrived**: the | ||
| destination registry is the interface, and Discovery simply scans it. So the problem | ||
| reduces to one thing: **replace the online transport with an offline one, and reuse | ||
| everything from the destination registry inward.** | ||
|
|
||
| [ADR 013's guiding principles](013-catalog-chaining.md#guiding-principles) carry over unchanged and make this clean: | ||
|
|
||
| - **OCM is the packaging format**; the OCM component descriptor is the authoritative | ||
| metadata. | ||
| - **Only OCM packages cross the boundary** — no Solar CRDs. Components and | ||
| ComponentVersions are derived on the destination side. | ||
| - The transfer layer is a **diode**. ADR 013's diode was a network component (ARC); | ||
| here it is **physical** — removable media hand-carried across the boundary. Solar is | ||
| agnostic to which medium is used. | ||
|
|
||
| ## Decision Drivers | ||
|
|
||
| - **No network path** may cross the boundary; the only channel is offline media. | ||
| - **Reuse ADR 013** wherever possible — do not re-decide the destination catalog model. | ||
| - **Untrusted channel**: physical media can be lost, swapped, or tampered with, so the | ||
| payload must be verifiable on its own — every OCM component version carries a signature | ||
| that the destination verifies (`ocm verify cv`) against a **trusted public key | ||
| provisioned out-of-band** (not on the same medium). | ||
| - **Air-gap autonomy**: operators inside the boundary should be able to work without | ||
| any dependency on the source environment. | ||
| - **Self-contained payload**: everything needed to deploy (descriptors, charts, | ||
| images) must be present locally; nothing may resolve to an external registry at | ||
| render or deploy time. | ||
| - **Incremental build-up**: the catalog grows over time as successive media arrive. | ||
|
|
||
| ## Considered Options | ||
|
|
||
| ### Transport across the boundary | ||
|
|
||
| - **Online OCI diode (ARC), as in ADR 013.** Not applicable: it requires a network | ||
| path across the boundary, which by definition does not exist in a full air-gap. | ||
| - **Offline OCM CTF on physical media (chosen).** A **by-value** transfer | ||
| (`ocm transfer ... --copy-resources --recursive`) writes a self-contained **Common | ||
| Transport Format** archive — component descriptors plus every referenced resource and image | ||
| *copied in*, not left as external references — to a directory/tar. The archive is carried | ||
| across on removable media and imported into the destination OCI registry with | ||
| `ocm transfer`. This preserves every ADR 013 invariant (OCM is the format, only OCM packages | ||
| cross, no Solar CRDs cross); only the carrier changes. | ||
|
|
||
| ### What crosses, and where selection happens | ||
|
|
||
| - **Ship the catalog (chosen).** The *full* catalog and its dependency | ||
| closure cross as one CTF. **Solar runs inside the air-gapped target**; Solar Discovery | ||
| scans the destination registry ([ADR 013 Option C](013-catalog-chaining.md#option-c-registry-scan-by-solar-discovery), verbatim) and operators select and | ||
| roll out **in-domain**. This is the "catalog build-up in the air-gap" the spike is | ||
| about, and it maximises air-gap autonomy. | ||
| - **Render then transport (alternative).** Selection and rendering happen | ||
| on the *connected* source; only the rendered rollout crosses; the destination is thin | ||
| (registry + FluxCD, no Solar catalog). Smaller payload and central control, but no | ||
| in-domain catalog or selection, and pre-rendered artifacts can go stale before they | ||
| are applied. | ||
|
|
||
| ## Decision Outcome | ||
|
|
||
| Support air-gapped chaining with **offline OCM CTF transport**, and make the | ||
| **ship-the-catalog** pattern primary. **For the ship-the-catalog pattern**, everything | ||
| from the destination registry inward is ADR 013 unchanged (Solar Discovery builds the | ||
| catalog, [Option C](013-catalog-chaining.md#option-c-registry-scan-by-solar-discovery)). | ||
| The render-then-transport alternative deliberately omits the catalog — it runs only a | ||
| registry and FluxCD, so Option C does not apply there. | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| autonumber | ||
|
|
||
| box rgb(232,244,253) Connected Domain | ||
| participant Src as Catalog Source | ||
| end | ||
|
|
||
| participant USB as USB Stick<br/>(OCM CTF — full catalog) | ||
|
|
||
| box rgb(253,237,236) Air-Gapped Domain | ||
| participant Reg as Registry<br/>(air-gap edge) | ||
| participant SolAr as SolAr<br/>(in target) | ||
| actor User as Operator | ||
| end | ||
|
|
||
| Note over Src,USB: Export the full catalog | ||
| Src->>USB: export all applications + dependencies | ||
| Note over USB: Complete offline catalog packaged by a<br/>NEW TOOL into one OCM CTF archive. | ||
|
|
||
| Note over Src,Reg: ✂ AIR-GAP BOUNDARY — USB stick carried across ✂ | ||
| Note over USB: No network path crosses the boundary —<br/>the USB stick is the ONLY transfer channel. | ||
|
|
||
| Note over Reg,User: Verify component versions, then seed the catalog | ||
| User->>User: ocm verify cv — each component version | ||
| alt any verification fails | ||
| Note over User: reject — nothing is imported | ||
| else all component versions verified | ||
| User->>Reg: import OCM CTF from USB | ||
| end | ||
| Note over Reg: Registry is the INTERFACE<br/>that backs the in-domain catalog. | ||
| SolAr->>Reg: discover available solutions | ||
| Reg-->>SolAr: catalog contents | ||
|
|
||
| Note over Reg,User: Select & roll out — inside the air-gap | ||
| User->>SolAr: pick applications + settings | ||
| SolAr->>Reg: resolve manifests | ||
| Reg-->>SolAr: pinned components | ||
| SolAr->>SolAr: deploy to target<br/>(FluxCD, gitless GitOps) | ||
| SolAr-->>User: applications running on-site | ||
| ``` | ||
|
|
||
| *Diagram source: [`img/016-airgap-ship-the-catalog.mmd`](img/016-airgap-ship-the-catalog.mmd).* | ||
|
|
||
| How it works, and what changes versus ADR 013: | ||
|
|
||
| 1. **Export (source).** A tool derives the package set from the source Solar catalog — | ||
| the full catalog plus its dependency closure — and `ocm transfer`s it **by value** | ||
| (`--copy-resources --recursive`) into a single, self-contained CTF archive. | ||
| *(This replaces [ADR 013's ARC Orders](013-catalog-chaining.md#option-a-1-solar-catalog-scan-by-arc); see "The export / | ||
| import tool" below.)* | ||
| 2. **Sign (source).** Each component version is signed with `ocm sign cv` — OCM signs the | ||
| component descriptors, not the archive blob — so the destination can establish trust | ||
| without a live connection to the source. | ||
| 3. **Carry across.** The archive crosses on removable media. No network path exists; | ||
| the medium is the only channel. | ||
| 4. **Verify + import (destination).** Every component version is verified with | ||
| `ocm verify cv` against the trusted public key (provisioned out-of-band); only then | ||
| does `ocm transfer` load the archive into the **destination registry** — the interface | ||
| that backs the in-domain catalog. | ||
| 5. **Build the catalog (destination).** *Unchanged from [ADR 013 Option C](013-catalog-chaining.md#option-c-registry-scan-by-solar-discovery):* Solar | ||
| Discovery scans the destination registry and creates Components / ComponentVersions. | ||
| 6. **Select and roll out (destination).** Operators select applications and settings in | ||
| the in-domain Solar; rendering happens locally against the target's current state, | ||
| and FluxCD reconciles from the destination registry ("gitless GitOps"). | ||
|
|
||
| **Keeping deployments self-contained.** A by-value transfer copies every resource into the | ||
| CTF, but **OCM does not rewrite the registry references embedded inside a chart's manifests** | ||
| (e.g. a Helm chart's `image:` values). Those are re-pointed at the destination registry at | ||
| **render time** by Solar Discovery / ocm-kit's Helm value templating — the mechanism from | ||
| [ADR 013's OCI URL re-mapping](013-catalog-chaining.md#oci-url-re-mapping) (and ADR 011). In | ||
| the ship-the-catalog pattern this happens in-domain, so a deployed workload resolves nothing | ||
| from the source or an external registry. The render-then-transport alternative renders on the | ||
| *source*, so its references must already target the destination registry before crossing — a | ||
| caveat of that pattern. | ||
|
|
||
| **The export / import tool.** Both scenarios name a "new tool". Its job is thin: on the | ||
| source, derive the wanted package set from the Solar catalog and write a signed CTF; on | ||
| the destination, verify and import. It wraps `ocm transfer` / ocm-kit rather than | ||
| inventing a new format. The exact package-set derivation (full catalog vs. a selected | ||
| subset) is the difference between the two scenarios; a detailed tool design is scoped | ||
| out below. | ||
|
|
||
| **Bootstrapping Solar into the air-gap.** The ship-the-catalog pattern runs Solar itself inside the | ||
| boundary, which is a chicken-and-egg: Solar's own images and Helm chart must arrive on | ||
| the *first* transport before it can serve a catalog. The initial seed therefore includes | ||
| Solar's components (as OCM packages); subsequent transports carry catalog content and | ||
| updates. | ||
|
|
||
| **Incremental build-up and GC.** OCM CTF transfer is additive: later archives bring new | ||
| or updated components, and the destination registry accumulates. This is exactly the | ||
| growth that [ADR 015 — Catalog and Registry Garbage Collection](015-catalog-registry-garbage-collection.md) | ||
| governs; availability reconciliation and the retention janitor apply to an air-gapped | ||
| destination the same as anywhere else. | ||
|
|
||
| ### Alternative pattern — render then transport | ||
|
|
||
| When in-domain selection is not needed, a lighter pattern selects and renders on the | ||
| connected source and ships only the rendered rollout; the destination runs just a | ||
| registry and FluxCD. ADR 013's destination catalog model | ||
| ([Option C](013-catalog-chaining.md#option-c-registry-scan-by-solar-discovery)) does | ||
| **not** apply here — there is no catalog on the air-gapped side, only reconciliation. The | ||
| exact artifact contract for these pre-rendered rollouts — CTF layout, OCI paths, and how the | ||
| destination `OCIRepository` pins each by immutable digest — is part of the deferred | ||
| export/import tool design (see *Scope*); and because rendering happens on the source, the | ||
| embedded registry references must already point at the destination registry before the CTF is | ||
| sealed. | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| autonumber | ||
|
|
||
| box rgb(232,244,253) Connected Domain | ||
| actor User as Operator | ||
| participant SolAr as SolAr | ||
| end | ||
|
|
||
| participant USB as USB Stick<br/>(OCM CTF) | ||
|
|
||
| box rgb(253,237,236) Air-Gapped Domain | ||
| participant Reg as Registry<br/>(air-gap edge) | ||
| participant Cluster as Target Cluster<br/>(FluxCD) | ||
| actor Ops as Air-gap Operator | ||
| end | ||
|
|
||
| Note over User,SolAr: Select & Render | ||
| User->>SolAr: pick target cluster + applications | ||
| SolAr->>SolAr: render manifests<br/>(pin OCM components) | ||
| SolAr-->>User: rollout package ready | ||
|
|
||
| Note over User,USB: Package onto transport medium | ||
| User->>USB: export as one OCM CTF archive | ||
| Note over USB: Collected by a NEW TOOL into a single,<br/>self-contained OCM CTF archive. | ||
|
|
||
| Note over SolAr,Reg: ✂ AIR-GAP BOUNDARY — USB stick carried across ✂ | ||
| Note over USB: No network path crosses the boundary —<br/>the USB stick is the ONLY transfer channel. | ||
|
|
||
| Note over Reg,Ops: Verify component versions,<br/>then load & reconcile | ||
| Ops->>Ops: ocm verify cv — each component version | ||
| alt any verification fails | ||
| Note over Ops: reject — nothing is imported | ||
| else all component versions verified | ||
| Ops->>Reg: import OCM CTF from USB | ||
| end | ||
| Note over Reg: Registry is the INTERFACE<br/>to the air-gapped side. | ||
| Cluster->>Reg: reconcile (poll OCI artifacts) | ||
| Reg-->>Cluster: manifests + images | ||
| Cluster->>Cluster: apply / prune<br/>("gitless GitOps") | ||
| ``` | ||
|
|
||
| *Diagram source: [`img/016-airgap-render-then-transport.mmd`](img/016-airgap-render-then-transport.mmd).* | ||
|
|
||
| Trade-offs versus the primary pattern: smaller payload and central control, but the destination | ||
| has no catalog and no in-domain selection, the operator on the connected side must know | ||
| in advance what the target should run, and a rendered rollout can go stale between | ||
| render and apply. It fits fixed, pre-defined deployments; it does not "build up" a | ||
| catalog in the air-gap. | ||
|
|
||
| ### Effect on ADR 013 | ||
|
|
||
| [ADR 013](013-catalog-chaining.md) remains the record for the connected case and is not modified. This ADR reuses | ||
| its destination model (Option C) verbatim and only substitutes the transport layer for | ||
| the air-gapped case. ARC stays a separate product, relevant when a (temporary) network | ||
| path exists; it is not required — and not present — in a full air-gap. | ||
|
|
||
| ### Consequences | ||
|
|
||
| Positive: | ||
|
|
||
| - Minimal new surface: only the transport changes; the destination catalog model, the | ||
| rendering pipeline, and gitless GitOps are all reused from ADR 013. | ||
| - Full air-gap autonomy (ship-the-catalog): operators select and deploy with no dependence on | ||
| the source environment. | ||
| - Self-contained and verifiable: a signed CTF needs no live trust path to the source. | ||
| - Medium-agnostic: the same signed OCM CTF payload works regardless of which removable | ||
| medium carries it. | ||
|
|
||
| Negative and trade-offs: | ||
|
|
||
| - **Fat payload (ship-the-catalog):** the full catalog plus dependency closure must be | ||
| transported, and re-transported for updates (CTF transfer is additive, which limits | ||
| this to deltas after the first seed). | ||
| - **Bootstrapping:** Solar must be seeded into the air-gap before it can serve a | ||
| catalog. | ||
| - **Manual, high-latency channel:** transport cadence is human-driven; there is no | ||
| real-time sync. | ||
| - **No feedback to source:** the source cannot observe destination state; any | ||
| reconciliation of "what the destination has" is out of band. | ||
| - **Signing is mandatory, not optional:** without a live source, per-component-version | ||
| OCM signatures — verified against an out-of-band trusted key — are the only trust | ||
| anchor. Provisioning and rotating that key is a deferred decision (see Scope). | ||
|
|
||
| ### Confirmation | ||
|
|
||
| - A CTF exported on the source and carried across (no network path) imports into the | ||
| destination registry and Solar Discovery builds the expected catalog — proving the | ||
| destination model is reused unchanged. | ||
| - Import is rejected when any component version fails `ocm verify cv` (untrusted-media case). | ||
| - In the ship-the-catalog pattern, an operator selects and rolls out an application entirely inside the | ||
| air-gap, with no source connectivity. | ||
| - A second, later CTF adds/updates components incrementally without re-seeding from | ||
| scratch. | ||
| - Nothing in a deployed workload resolves to an external registry (self-contained | ||
| payload). | ||
|
|
||
| ## Scope | ||
|
|
||
| Decided here: that air-gapped chaining is supported, and that it works by offline | ||
| signed OCM CTF transport with the destination model reused from ADR 013, with the | ||
| ship-the-catalog pattern as primary. | ||
|
|
||
| Out of scope / follow-up: | ||
|
|
||
| - **Detailed export/import tool design** — package-set derivation, CTF layout, signing | ||
| workflow, incremental-delta computation. | ||
| - **A leaner in-air-gap Solar footprint** ("SolAr light") — deliberately deferred. | ||
| - **Registry/catalog GC in the air-gapped destination** — governed by | ||
| [ADR 015](015-catalog-registry-garbage-collection.md); not re-decided here. | ||
| - **One-way data-diode boundaries** — a data diode is a controlled *network* path, not | ||
| a full air-gap; it is a different boundary model with its own threat model and belongs | ||
| in a separate ADR, not here. | ||
| - **Trusted-key provisioning and rotation for boundary signing** — imports are verified | ||
| with `ocm verify cv` against a trusted public key that must reach the destination | ||
| out-of-band; the key-management mechanism (provisioning, rotation, per-source vs. | ||
| shared) is a separate decision, not settled here. [ADR 014](014-artifact-signing.md) | ||
| covers rendered-artifact signing at deploy time — a different layer. | ||
|
|
||
| ## More Information | ||
|
|
||
| - Diagrams: [`img/016-airgap-ship-the-catalog.mmd`](img/016-airgap-ship-the-catalog.mmd) | ||
| (primary), [`img/016-airgap-render-then-transport.mmd`](img/016-airgap-render-then-transport.mmd) | ||
| (alternative). | ||
| - [ADR 013 — Solar Catalog Chaining via ARC](013-catalog-chaining.md) — the connected | ||
| case; its [Option C](013-catalog-chaining.md#option-c-registry-scan-by-solar-discovery) | ||
| destination model is reused here. | ||
| - [ADR 014 — Solar Artifact Signing](014-artifact-signing.md) — a *complementary* layer: | ||
| cosign signing of *rendered artifacts* verified by FluxCD at deploy time; it does **not** | ||
| cover OCM package signing at the import boundary (see Scope). | ||
| - [ADR 015 — Catalog and Registry Garbage Collection](015-catalog-registry-garbage-collection.md) | ||
| — governs the accumulating destination registry. | ||
| - [Spike #581](https://github.com/opendefensecloud/solution-arsenal/issues/581); | ||
| decision on whether to support the use case tracked in odd-internal#70. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: opendefensecloud/solution-arsenal
Length of output: 14010
🏁 Script executed:
Repository: opendefensecloud/solution-arsenal
Length of output: 21549
🏁 Script executed:
Repository: opendefensecloud/solution-arsenal
Length of output: 27091
Require rendered-artifact verification before deployment.
ocm verify cvauthenticates OCM component descriptors. ADR 014 separately requires FluxCD to verify signed rendered OCI artifacts throughOCIRepository.spec.verify. Add this contract and its failure path before reconciliation and apply in both ADR 016 workflows, including the primary ship-the-catalog flow. Do not treat OCM verification as rendered-artifact verification unless ADR 016 defines that equivalence.📍 Affects 2 files
docs/developer-guide/adrs/016-airgapped-catalog-chaining.md#L239-L244(this comment)docs/developer-guide/adrs/img/016-airgap-render-then-transport.mmd#L33-L39🤖 Prompt for AI Agents