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 groups — ORBITALREG_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.

OrbitalReg ships under the OrbitalReg EULA with a Free Forever tier.