Skip to content

Core concepts

This page walks the data model and the lifecycle of a single artifact end-to-end. It's the document you read once and then re-skim when you're trying to remember what a "repo" is in OrbitalReg's vocabulary.

Project

A project is the top-level governance boundary. A typical deployment has 5–500 projects: one per team, one per product, one per "line of business" — pick the granularity that matches how your existing IAM groups are scoped.

Projects own:

  • A set of repositories
  • A retention policy default (overridable per repository)
  • A license policy
  • A quarantine policy
  • A list of project-owners (full admin within the project)
  • A list of project-editors (push but not policy-edit)

A project's slug is unique platform-wide. Renames are in-place — the UUID stays stable so any downstream referencing FK doesn't break.

Projects are the isolation boundary within a deployment — a deployment itself is single organization. See Tenancy model for what that means and why.

Repository

A repository is a typed package store inside a project. Each repository has a single format (Maven, npm, PyPI, Docker, …) and is either local (uploads only), remote (proxy + caching mirror for an upstream), or virtual (federated view of multiple underlying repos).

The slug is (project, repo)-unique, not platform-unique, so acme-platform/maven and acme-finance/maven are both valid.

Artifact

An artifact is a single uploaded version. Identity is the tuple (repo_id, path, version):

  • path is format-specific — for Maven, com/example/my-lib; for npm, @scope/pkg; for Docker, library/alpine.
  • version is the format's version string. SemVer for npm, Maven coordinates for Maven, OCI tag/digest for Docker, etc.

The artifact row carries the SHA-256 digest of the bytes, the content-type, and a JSONB metadata blob with format-specific fields.

Scanning and Detection

Every upload triggers a scan job. The scan-job worker pool runs the configured scanners against the artifact and writes findings to scan_findings. A finding is a tuple of:

  • (artifact_id, scanner, vuln_id, severity, license_id, …)
  • vuln_id is the canonical CVE / GHSA / OSV id when available
  • severity is normalised to CVSS-style buckets (CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN)

A single artifact typically produces 10–100 findings — most are informational. The interesting ones are the ones above the project's quarantine threshold; those flip the artifact's quarantined bit.

A quarantined artifact:

  • Still appears in listings (operators need to see it to triage)
  • Returns HTTP 451 on download with a "why-blocked" envelope
  • Cannot be promoted across repository boundaries

Security blocks

Detection is reactive — the scanner found something. Security blocks are proactive — an admin authored a rule that says "do not serve artifacts matching this pattern, with this customer-facing message":

yaml
block_type: sha256
pattern:    "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
reason:     "Confirmed-malicious sample from FAS-2025-0042"
customer_message: |
  This artifact was blocked because it matches a known supply-chain
  attack indicator. Contact security@example.com if you believe this
  is in error.

Block types: path (exact match against the request path), sha256 (content-hash match), pattern (a path.Match glob against the request path), package, and auto_cve (written automatically by auto-quarantine, matched like path). Blocks fire on every download path — Maven, npm, PyPI, Docker, the lot.

package blocks match a full path segment, not a substring. The pattern must equal one of the segments of the request path bounded by / on both sides — this is a heuristic that works because every format adapter puts the package name in its own segment, but the segment boundary means a pattern that includes the version (like log4j-core-2.14.1) will never match, because the version is part of the file name segment, not a segment of its own. Drop the version:

EcosystemExample request pathMatching package patternPattern that never matches
Mavenorg/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jarlog4j-corelog4j-core-2.14.1
npmnpm/<project>/<repo>/lodash/-/lodash-4.17.21.tgzlodashlodash-4.17.21
NuGetnuget/<project>/<repo>/v3-flatcontainer/contoso.utils/1.0.10/contoso.utils.1.0.10.nupkgcontoso.utilscontoso.utils.1.0.10

If you need to block one specific version, use a pattern block with a glob (e.g. **/log4j-core-2.14.1.jar) or a path block with the exact path instead.

The public /blocked/<id> page (no auth) renders the customer message so a developer who ran npm install and got blocked has somewhere to go.

Retention

A retention policy is a per-repository rule that prunes old versions on a schedule:

  • keep-N-newest — keep the latest N versions of each path
  • keep-by-age — keep versions newer than N days
  • Combinations are allowed; the union of all matching rules survives

Pruned artifacts soft-delete first (visible in Trash for 30 days), then hard-delete the row + S3 object.

Retention runs hourly. The retention_audit table records every deletion with the rule that triggered it, the artifact metadata, and the operator (or service account) who installed the rule.

Promotion

Promotion moves an artifact between repositories under a gate. The gate has three sub-checks:

  1. CVE policy — no findings above the project's threshold
  2. License policy — every detected license appears in the project's allow-list
  3. Scan rule policy — no scanner-specific custom rules failing

A promotion request that passes the gate runs as a copy + a write to promotion_audit; one that fails returns the failing checks so CI can decide whether to block the build or surface a warning.

Sigstore trust policies

When you wire OrbitalReg into a CI flow that signs artifacts via cosign keyless, the verifier needs to know which (identity_pattern, issuer_pattern, fulcio_root, rekor_url) tuples it trusts. Those live in sigstore_trust_policies and are managed under Admin → Sigstore → Trust policies.

Service accounts + tokens

Service accounts are non-human principals scoped to a project. Each can have one or more tokens (the actual orbsa_… bearer strings used in Authorization: Bearer … headers).

For CI, prefer the OIDC token exchange flow — it eliminates long-lived bearer tokens entirely. See OIDC token exchange.

Who sees what

Project visibility is least-privilege: a non-org_admin user sees only the projects they have an explicit role_bindings row for — either a direct binding on that project, or an org-wide binding (project_id IS NULL) that grants every project.

There are two independent ways a user gets a role, and only one of them creates visibility:

  • Config-mapped groupsORBITALREG_AUTH_ADMIN_GROUPS / _DEVELOPER_GROUPS / _READER_GROUPS (auth.adminGroups / developerGroups / readerGroups in the Helm values) map an IdP group claim to a role name at login time. This decides what a user is allowed to do once they can see a project, and grants org_admin its all-projects visibility — but membership in readerGroups/developerGroups by itself creates norole_bindings row.
  • role_bindings rows — created explicitly (project owner adding a member, an org-wide grant, or SCIM group provisioning into user_groups) — are what VisibleProjectIDs actually filters on.

The practical effect: a freshly-provisioned SSO user who is only in the readerGroups claim, with no role_bindings row yet, logs in to a completely empty project list — not a bug, but easy to mistake for one. The /api/v1/whoami response's hasAnyBinding flag (true for org_admin, an org-wide binding, or any project-scoped row) lets the frontend distinguish "no projects exist yet" from "you have no access yet" and show an explanatory empty state instead of a bare table. Give new non-admin users an explicit role_bindings row (project membership or an org-wide grant) — don't rely on group mapping alone to grant them visibility.

Webhook subscriptions

Outbound events: artifact uploaded, artifact promoted, finding above threshold, retention deletion, etc. Each subscription has:

  • A target URL (HTTPS only, validated at CRUD time)
  • A shared HMAC secret (write-only — list responses return has_secret: bool)
  • An event filter (subset of the published catalog)
  • A delivery target type — generic webhook, Slack, Microsoft Teams

The full event catalog lives at Webhook events.

Released under the Apache-2.0 License.