Plan: a private conda channel for pixi, with supply-chain controls¶
Status: design for review, 2026-10-06. Implementation starts against this plan; gates below define done.
Companion to the Rocky Linux image mode walkthrough. Code lands in private-conda-channel-pixi/ of this repo.
Who this is for¶
Platform and security teams at large organizations that run Python and data science on conda, already front everything with an artifact manager, and have to answer auditors: where did every package come from, who published it, was it verified before it was installed, and can we prove the build used nothing else. The requirements below are drawn from what such teams have asked for in public (registry and client issue trackers, published internal tooling), not from any one company.
Requirements the walkthrough must demonstrate¶
| # | Requirement | Where it is demonstrated |
|---|---|---|
| R1 | Credentials never appear in channel URLs, manifests or lockfiles; clients send an Authorization header from a credential store keyed by host |
pixi auth store, RATTLER_AUTH_FILE in CI, lock inspection |
| R2 | Bearer tokens work for reads and publishing; per-repository, read-only tokens for consumers | Artifact Keeper repo tokens |
| R3 | A proxy of conda-forge, a hosted internal channel, and one virtual channel with predictable priority | three repositories plus conda-virtual |
| R4 | Internal package names cannot be shadowed by the public proxy (dependency confusion) | name-ownership guard in the virtual channel, negative test |
| R5 | Correct repodata formats: .conda and .tar.bz2, repodata.json, .zst, .bz2, CEP-16 shards, fast fallbacks |
pixi against every format; shard index consumed by rattler |
| R6 | Proxy metadata freshness with an admin-set TTL and no silent staleness; failed members fail loudly | proxy TTL, virtual member failure test |
| R7 | Immutable artifacts (no overwrite), promotion by repointing, withdrawal with a notice, audit of pulls and pushes | 409 on re-upload, staging to internal promotion, CEP-6 notice, download records |
| R8 | CEP-27 publish attestations stored and served per CEP-50, verified by the registry on upload and by the consumer before linking | attestation upload, .sigs sidecars, attestations_sha256 in repodata, verification gate in the container build |
| R9 | Enterprise trust policy for attestations: configurable identity and issuer, key-based bundles for on-premises CI, not only public GitHub OIDC | registry trust-policy settings, key-based signing in the demo CI |
| R10 | Server-set indexed_timestamp so exclude-newer cooldowns are trustworthy |
repodata field, pixi exclude-newer |
| R11 | SBOM, PURLs, vulnerability and license scanning for what is served and what is installed; blast-radius lookup from a PURL to environments | hosted scanning, environment SBOM from pixi.lock, reverse lookup |
| R12 | Registry-only builds: the build network has no internet, the lockfile is portable across mirrors, offline install works from the lock | internal podman network, mirrors config, pixi install --frozen --offline |
| R13 | Everything runs in containers with a bare hostname and TLS from an internal CA | ak.internal on the compose network, Caddy internal CA, tls-root-certs |
Environment¶
- Registry: Artifact Keeper built from
mainplus the fixes below (this is 1.11.0 work), run as its own compose projectak-condaon an internal podman network. Caddy answers onhttps://ak.internal(port 443 on the network, not on the host). The Caddy internal CA is exported and trusted by every client container. Reason: pixi looks credentials up by bare hostname and drops the port, and large organizations do not send credentials over HTTP. - Clients: every pixi and rattler-build step runs in a container on the same network. A second
network,
build-isolated, hasinternal: true(no route to the internet) and is used for the registry-only proofs. The only container with internet access is Artifact Keeper itself. - Repositories:
conda-forge(formatconda, remote, upstreamhttps://conda.anaconda.org/conda-forge)conda-internal(hosted, visibilityinternal): released internal packagesconda-staging(hosted): where CI publishes; promotion toconda-internalis gatedconda-virtual(virtual:conda-internalpriority 1,conda-forgepriority 2): the one channel consumers usepypi-remote(remote PyPI proxy) so mixed conda and PyPI projects also resolve through the registryoci-apps(hosted OCI) for the built application images- Internal packages, built with
rattler-buildfromrecipe.yaml, in a container: acme-core(noarch python): a small libraryacme-fastmath(linux-64, compiled): exercises a real subdiracme-report(noarch): depends onacme-corepluspandasandrichfrom conda-forge All names carry the reserved prefixacme-.- Consumer project:
pixi.tomlwithchannels = ["https://ak.internal/conda/conda-virtual"],channel-priority = "strict",exclude-newer = "14d", internal dependencies pinned withchannel = "https://ak.internal/conda/conda-internal", one PyPI dependency, and apixi.lock. - Container build: the pixi-on-UBI-micro multi-stage pattern. Builder:
ghcr.io/prefix-dev/pixipinned by digest (through the registry's proxy),/etc/pixi/config.tomlwith mirrors pointing atak.internal, credentials via a podman secret asRATTLER_AUTH_FILE,pixi install --locked, then an attestation verification gate over every package inpixi.lock. Runtime: UBI micro with the environment and apixi shell-hookentrypoint. Image signed with cosign and pushed tooci-apps. - Scanning: Syft with the conda cataloger plus Grype on the built environment; the registry's
hosted scan results;
POST /api/v1/sbom/environment?filename=pixi.lockand the environment register with PURL reverse lookup.
Decisions¶
- One virtual channel, and make it correct. Two channels in pixi (internal, then proxy) works today, but it leaves dependency confusion to client discipline. Large organizations want one channel URL that is safe by construction. So the virtual channel is the product story, and the fixes below make it hold: hosted-owns-the-name guard, compressed and capped member fetch, loud member failure, hosted-first merge.
- Key-based attestations for the demo, keyless for GitHub. Public Sigstore with a GitHub Actions
issuer is what works on
maintoday. Regulated shops often cannot use public Sigstore; they sign with their own keys or a private Sigstore. The demo CI signs CEP-27 predicates with a cosign key (cosign attest-blobproducing a Sigstore bundle, no public transparency log) and the registry is configured with that public key as its trust policy. The GitHub Actions keyless path stays supported and is documented. - Attestations are served, not only stored. CEP-50 sidecars (
<file>.sigs,.sigs.<sha256>) andattestations_sha256in repodata, readable anonymously wherever the package is. Consumers verify before linking; the container build does this withcosign verify-blob --bundleover every locked package, and the docs point at rattler's built-in verification as pixi adopts it. - Promotion is the gate. CI publishes to
conda-staging. Promotion toconda-internalrequires: a verified attestation, a passed vulnerability scan, and a license on the allow list. Consumers never see staging. Rollback is withdrawing with a CEP-6 notice, not overwriting. - Shards are in scope for the hosted channel, stretch for the proxy. Hosted shards must be
consumable by rattler (fix the encoding). Shards through the proxy (#4177) are attempted if time
allows; otherwise
disable-shardedis set for the proxy URL in the client config and the gap is documented. - Everything the client needs to trust is served by the registry: the CA certificate, the
attestation verification key, and the SBOMs, each from a generic repository
trust.
Artifact Keeper work for 1.11.0¶
Found by the audit of main (5e351fc) and the client research; each becomes an issue (or links an
existing one) and a fix branch. Fixes are prepared locally, built into the demo image, and listed for
Brandon's approval before any PR is opened.
| # | Item | Existing issue | Gate |
|---|---|---|---|
| F1 | Virtual channel: hosted members own their package names; remote versions of a name present in a hosted member are excluded from the merge (same guard other formats have) | new | G4 |
| F2 | Virtual channel: fetch members compressed with a byte cap and streaming; hosted-first merge order | #4180 | G3, G5 |
| F3 | Virtual channel: a member that fails to fetch or parse fails the request loudly instead of being dropped | #4192 | G6 |
| F4 | CEP-16 shard index: encode hashes as raw bytes per the spec; add a rattler-based test | #4173 (partial) | G5 |
| F5 | CEP-50: serve .sigs sidecars and attestations_sha256; anonymous GET .../attestation on readable repos |
new (#4033 epic) | G8 |
| F6 | Attestation trust policy: configurable OIDC issuers and identities, and key-based Sigstore bundles with a configured public key | new (#4033 epic) | G8, G9 |
| F7 | Attestation state feeds policy and promotion: min_attestation_state and require_signature recognize a verified CEP-27 attestation |
new | G7 |
| F8 | Upload validates the subdir against index.json; POST without X-Conda-Subdir uses the package's own subdir |
new | G2 |
| F9 | Promotion copies artifact metadata (depends, md5, attestation) | new, verify first | G7 |
| F10 | indexed_timestamp set by the server on every repodata record (CEP-47) |
new | G10 |
| F11 | Accept rattler's /t/<token>/ URL layout (/t/{token}/conda/{repo}/...) |
new | G1 |
| F12 | Site docs: a conda and pixi page with the real upload API, conda versus conda_native, auth, mirrors |
site, new | docs |
Out of scope for this walkthrough, noted in the docs: scan-on-proxy for conda (#4097), quotas (#4422), JLAP patches (#4175), and the full scale epic (#4172).
Gates¶
| # | Gate | Proof |
|---|---|---|
| G1 | Auth: pixi auth login ak.internal --token and RATTLER_AUTH_FILE both work; pixi.lock and pixi.toml contain no credentials; a token in the URL is accepted by the registry but the docs show the header form |
lock grep, registry access log |
| G2 | Publish: rattler-build upload artifactory PUTs three packages to conda-staging with a Bearer token; re-upload returns 409; a linux-64 package uploaded without a subdir header lands in linux-64 |
HTTP transcripts |
| G3 | Resolve: a pixi install --locked of the consumer project through conda-virtual succeeds on a network with no internet; the lock records conda-forge URLs, downloads hit the proxy |
pixi output, registry download records |
| G4 | Dependency confusion: acme-core 99.0 published to the remote member's upstream is not offered by conda-virtual; the solve keeps the internal version |
repodata diff, solve output |
| G5 | Formats: pixi consumes zst, bz2, json and the hosted shard index without falling back; proxy shards pass through or disable-sharded is documented |
request log |
| G6 | Freshness and failure: a new conda-forge package appears within the TTL; a broken member makes the virtual channel fail loudly | logs |
| G7 | Promotion: an un-attested or vulnerable package in conda-staging is refused promotion; the attested, clean package promotes with its metadata intact and resolves from conda-internal |
promotion API responses, depends check |
| G8 | Attestations: CEP-50 sidecars are served; the container build's verification gate passes for signed packages and fails the build for a tampered or unsigned one | build logs |
| G9 | Trust policy: an attestation signed by the wrong key or a disallowed identity is rejected on upload | 400 responses |
| G10 | Cooldown: repodata carries server-set indexed_timestamp; exclude-newer excludes a package indexed minutes ago |
solve output |
| G11 | SBOM and blast radius: SBOM from pixi.lock via the registry; Syft plus Grype on the built image; a PURL lookup returns the environments that contain it |
API responses, scan output |
| G12 | Offline: pixi install --frozen --offline from a pre-filled cache succeeds with the network removed; a package with a flipped byte fails the sha256 check |
output |
| G13 | Image: the application image builds from the registry only, is signed, and runs | podman output, cosign verify |
UI track: what the screenshots must show¶
Each screen below is a screenshot in the walkthrough. Where the web UI (artifact-keeper-web)
cannot show it today, that is a UI work item for 1.11.0, prepared the same way as the backend fixes.
| # | Screen | Shows | Likely UI work |
|---|---|---|---|
| U1 | Repositories list filtered to conda | the four channels with type (remote, hosted, staging, virtual) and visibility | none expected |
| U2 | conda-virtual detail |
members in priority order, with the name-ownership rule stated | member priority display; a note that hosted members own their names |
| U3 | conda-internal artifact list |
packages grouped by name with subdir, version, build, size | subdir column if missing |
| U4 | Package detail for acme-report |
depends, license, sha256, uploader, upload time, and the attestation: verified, identity, issuer or key, verification time, link to the sidecar | attestation panel (new) |
| U5 | Scan results for a conda package | Grype/Trivy findings per component, PURLs | none expected |
| U6 | Promotion from conda-staging to conda-internal |
the gate decision: attestation verified, scan passed, license allowed; and the refusal for the un-attested package with the reason | gate-result display per rule (likely new) |
| U7 | Withdrawn package | the CEP-6 notice text and the removed state |
notice display if missing |
| U8 | Environments | a registered pixi.lock, its SBOM, and the PURL reverse lookup result (blast radius) |
environment list and lookup UI if missing |
| U9 | Download audit for a package | who pulled what and when | none expected |
| U10 | Trust settings | the attestation trust policy (issuers, identities, key) and the repodata signing key | settings page for the policy (new) |
Screenshots are taken headlessly against the demo stack, light theme, cropped to the panel, and
stored under docs/private-conda-channel-pixi/images/.
Walkthrough outline¶
- Why one channel: the enterprise problem statement.
- Stand up the registry on a private network with TLS and a bare hostname.
- Create the channels: proxy, hosted, staging, virtual; tokens for CI and for consumers.
- Configure pixi for registry-only: auth store, mirrors, strict priority, cooldowns, pinned internal names.
- Build and publish internal packages with rattler-build; attest them.
- Promote with gates: scan, license, attestation.
- Resolve and lock through the virtual channel; prove no other source was reached.
- Build the application container on an isolated network; verify attestations before linking; sign the image.
- SBOM, scanning and blast radius.
- The negative tests: confusion, overwrite, tampering, wrong key, offline.
- What changed in Artifact Keeper for this (1.11.0) and what is next.