Skip to content
GitHub

Supply-chain security

Understand Depsilo v0.11.0 minimum release age, malicious-package blocking, tamper detection, freeze snapshots, and audit integrity.

Depsilo places policy between package managers and upstream registries. v0.11.0 provides these independent controls:

Control New-install default Result Primary boundary
Minimum release age Off Young versions return 451 QUARANTINED Timestamp provenance is bound per ecosystem; Go and APT have no usable source
Known-malicious blocklist On, sync every 6 hours Matched versions return 451 MALICIOUS_BLOCKED Covers OSV MAL-* records for 8 ecosystems
Tamper detection On Retains a first-seen hash and raises a critical alert; no 451 Passive trust-on-first-use
Freeze / golden snapshot Off In snapshot mode, unapproved versions return 451 SNAPSHOT_BLOCKED Covers only artifacts already in the cache
Audit integrity and SIEM routing Chain on; routing needs configuration Audit rows are hash-chained and verifiable offline, and can be forwarded to collectors A full-database rewrite is detectable only with an external anchor

These controls are independent. None of them changes caching, upstream selection, or proxy behavior.

Minimum release age refuses versions that are too young on the request path. Depsilo binds “artifact identity + timestamp + the upstream that will serve the bytes” per ecosystem, and only accepts a positive threshold where the timestamp source can be proven:

Ecosystem Timestamp source Enablement
npm The packument time[version], carried in the authenticated tarball token Available directly
PyPI upload-time from the same upstream’s PEP 691 JSON simple index The upstream must serve JSON; HTML-only upstreams fail closed
Composer The p2 metadata entry’s own time field Available directly; clients may fall back to the original dist URL after a 451
NuGet The v3 registration index’s published timestamp Available directly
Cargo The sparse index’s pubtime and cksum Available directly
RubyGems The compact index’s created_at and checksum Available directly
Conda, CRAN, Maven, Alpine, Helm The artifact response’s Last-Modified (approximate) Must be acknowledged in supply_chain.approximate_sources
Docker When this instance first saw the digest (first observation, not a publish time) Must be acknowledged in supply_chain.observation_sources
Go, APT No usable publish-time authority Keep the threshold at 0
[supply_chain]
min_release_age_enabled = true
mode = "block" # or "warn": record and serve, for an observation period
fail_closed = true
approximate_sources = ["conda", "cran", "maven", "alpine", "helm"]
observation_sources = ["docker"]
# allow = ["npm:internal-*"] # bypass by ecosystem prefix; bypasses are audited
[supply_chain.min_release_age]
default = "0"
pypi = "3d"
npm = "7d"
cargo = "3d"

Behavior and boundaries:

  • Decisions run before the cache lookup, so already-cached versions cannot dodge a new policy.
  • Missing provenance fails closed. A positive threshold for an unbound or unacknowledged ecosystem is rejected at startup instead of silently degrading to “no check”.
  • Administrators can approve a single (ecosystem, package, version); every approval, revocation, and allow-list bypass is audited.
  • Docker’s age starts when this instance first pulls the digest and cannot be forged by the publisher; unseen digests stay quarantined until the threshold passes, and a registry that omits Docker-Content-Digest is refused.
  • Approximate ecosystems trust the serving upstream’s Last-Modified. Composer clients can fall back around a 451 to the original dist URL, so that gate remains best-effort.

The malicious-package blocklist is enabled by default:

[supply_chain.blocklist]
enabled = true
sync_interval = "6h"
# mirror_url = "https://osv-vulnerabilities.storage.googleapis.com"
# proxy = "http://127.0.0.1:7890"

v0.11.0 synchronizes MAL-* records from the OSV malicious-packages dataset for npm, PyPI, Cargo, RubyGems, Composer, NuGet, Go, and Maven. It imports explicit affected-version lists and all-version advisories. Bounded ranges without an explicit version list are skipped because Depsilo does not infer those ranges.

While the dataset covers an ecosystem, an artifact whose identity cannot be proven is refused rather than served unchecked: PyPI legacy archives (.zip, .egg, .tar.bz2, …) resolve identity from the simple index that declares them, and RubyGems resolves the exact gem identity from the compact index, with platform gems inheriting their base version’s record.

Matching happens before release-age and allow-list checks. A matching artifact already in cache is no longer reachable through this gate, although its bytes remain in storage until LRU or operator cleanup. For a confirmed false positive, an administrator can create an audited override lasting at most 24 hours. It cannot be extended in place; continued access requires another recorded decision.

Synchronization fails open in a bounded way:

  • Existing local data remains active after a failed refresh.
  • An empty local dataset blocks nothing.
  • A synchronization error does not stop the entire proxy from serving packages.
[supply_chain.tamper_detection]
enabled = true

For artifacts treated as immutable, Depsilo records SHA-256 during the first streaming fetch. If a natural background refresh later returns different bytes for the same version, Depsilo refuses to replace the cached content, keeps the first-seen cached copy, and writes a critical tamper_detected event. It can also notify a configured webhook. If LRU has already evicted the first-seen copy, a later cache miss stores and serves the re-fetched bytes before comparing them with the retained hash baseline. Depsilo alerts, but the old copy can no longer be restored.

The limitations are as important as the alert:

  • Detection is passive. It compares bytes only during a normal background refresh or a re-fetch after eviction, and does not actively probe upstream.
  • It is an alerting mechanism, not a generic blocking policy, and does not return 451.
  • “First seen” is a local baseline, not a signature or transparency-log proof. If the first fetch is already compromised, its hash becomes the baseline.
  • After LRU evicts the original cached bytes, inconsistent new bytes are already stored and may be served before verification. Depsilo can still alert using the old hash, but it cannot restore the old artifact.

Promote the artifact set currently in cache into a named, hash-pinned snapshot, then serve only the versions inside it:

  • Promote the cache into a named snapshot from the Admin cache page or the Admin API, and export or import the depsilo/snapshot/v1 manifest.
  • With snapshot-only mode enabled, anything outside the active snapshot is refused with 451 SNAPSHOT_BLOCKED after the malware gate, and membership lookups fail closed.
  • Snapshots pin the SHA-256 values recorded by tamper detection. Importing a manifest seeds missing baselines, so a first fetch that differs from the manifest still raises a tamper alert.

Audit rows written since schema v8 are hash-chained (prev_hash / hash, UTC-normalized timestamps):

  • The audit page can re-verify on demand; GET /api/v1/admin/audit/integrity returns the full report; and depsilo audit verify [--json] recomputes the whole chain offline against the SQLite file.
  • Rows written by earlier versions are reported as a pre-chain prefix instead of being presented as covered.
  • Editing, deleting, or re-ordering a chained row names the first broken id.

The chain alone cannot detect a full-database rewrite, because an attacker can recompute it. Writing chain-head checkpoints outside the database closes most of that gap: [audit] checkpoint_file appends NDJSON checkpoints with fsync and cross-checks them during verification, while [audit] checkpoint_url POSTs the same line to a WORM gateway or log platform, retrying after 60 seconds on failure; delivery status (remote_head_id, remote_last_success_at, remote_last_error) is visible in the integrity API and the audit page.

SIEM routing forwards the same audit stream to external collectors as NDJSON or Splunk HEC envelopes. Each exporter keeps its own cursor and delivers at least once: a collector outage stops the cursor instead of dropping records, restarts resume from the last successful batch, and failures back off from 10 seconds to 5 minutes. Filters select events by action and outcome (blocked, miss, …), and new exporters start at the current audit head. The Admin API and the audit page cover CRUD, enable/disable, lag and last-error status, and a test delivery through the real path.

The runtime SBOM export in multi-project workspaces offers a CRA mode and a technical-file preset: it carries purl, the SHA-256 recorded by tamper detection, supplier/license fields, and product-to-component dependency relationships. Supplier and license values come from operator-declared [compliance.components] annotations; unlisted components export as unknown, and coverage gaps are reported instead of invented. With a configured Ed25519 key, sign=true returns a detached signature over the response body.

Common bypass paths include:

  • A direct additional pip index. Do not point --extra-index-url at an uncontrolled upstream.
  • A Go GOPROXY fallback such as |direct that connects directly after an error.
  • Composer may fall back to the original dist URL when a mirrored dist is refused. Hard blocking requires restricting client access to those original download hosts.

Depsilo can preserve ecosystem checksums or signatures, but it cannot turn an unsigned artifact into one with proven provenance. It does not replace upstream signing, build attestations, lockfiles, or SBOMs. Minimum release age binds a timestamp source per ecosystem; the blocklist rejects known records; tamper detection reports same-version byte changes; snapshots limit which versions can be served. None is a complete proof that a package is safe.

Quarantine blocks, allow bypasses, approvals and revocations, malicious package blocks and temporary overrides, tamper events, and snapshot blocks are recorded in the database. Admin can filter them by ecosystem, action, and package, verify integrity, and forward them to a SIEM as described above. With webhooks configured, quarantine and malicious-package blocks can notify immediately, while tamper mismatches use critical severity.

Before rollout, verify at least once that:

  1. A positive threshold for a source-bound ecosystem (for example npm) returns 451 QUARANTINED for a young version and leaves an event behind.
  2. When approximate or first-observation ecosystems are enabled, startup accepts them and the capability summary shows approximate / observed or the matching safety-paused state instead of an all-green result.
  3. A reviewed Package Rule or the malicious dataset returns 451 and leaves an audit record.
  4. With snapshot mode active, a version outside the snapshot returns 451 SNAPSHOT_BLOCKED.
  5. depsilo audit verify passes and the checkpoint lands outside the database’s failure domain.
  6. Clients have no fallback registry or direct network route around the proxy, and audit records and webhooks reach the intended operator channel.