Skip to content

Release pipeline ​

OrbitalReg ships through four customer channels (Helm-Repo, Container images, Operator manifest, Air-gapped tarball) plus a vendor-side license-issuer CLI. This page covers Phase A (multi-arch container images), Phase B (Helm chart publish), Phase C (operator one-click install manifest), Phase D (air-gapped delivery tarball), and Phase E (vendor-side license-issuer CLI). Phases A–D run from the same release.yml workflow on every CalVer tag; Phase E ships as a standalone Go module under tools/license-issuer/, invoked by the vendor outside the tagged release flow because per-customer envelopes are bound to a specific install UUID and have to be issued one at a time.

What gets built ​

For every tag matching YYYY.MAJOR.MINOR (or YYYY.MAJOR.MINOR-rc.N / -pre.N), the release.yml GitHub Actions workflow publishes:

ComponentImageBuilt fromRun as
APIghcr.io/<owner>/orbitalreg-api:<version>api/Dockerfiledistroless nonroot (UID 65532)
Frontendghcr.io/<owner>/orbitalreg-frontend:<version>frontend/Dockerfilenginx-unprivileged (UID 101)
Operatorghcr.io/<owner>/orbitalreg-operator:<version>tools/k8s-operator/Dockerfiledistroless nonroot (UID 65532)

Each image is built for both linux/amd64 and linux/arm64 and combined into a single OCI manifest list, so a Customer cluster running on Apple-Silicon dev nodes or Graviton prod nodes pulls the right architecture transparently.

Stable tags additionally advance the moving :latest tag. Pre-release tags (-rc.N, -pre.N) publish the versioned tag but never advance :latest.

Provenance chain ​

The workflow signs and attests every published image so a downstream auditor can verify the build came from this repository's release.yml without holding any long-lived OrbitalReg key:

  1. Keyless signature. cosign sign mints a short-lived Fulcio certificate against the workflow's GitHub-Actions OIDC token and logs the signature in the public Rekor transparency log.

  2. CycloneDX SBOM. syft walks each architecture's layers and emits a CycloneDX 1.5 JSON SBOM. cosign attest --type cyclonedx then binds the predicate to the manifest digest as a Sigstore in-toto statement.

  3. Build provenance. docker/build-push-action is invoked with provenance: true and sbom: true, attaching SLSA build-provenance plus a buildx-native SBOM to the manifest list itself.

The Rekor log entries are public; nothing about verification requires network access to OrbitalReg infrastructure.

Verifying an image ​

bash
cosign verify \
  --certificate-identity-regexp 'https://github.com/<owner>/<repo>/' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/<owner>/orbitalreg-api:2026.1.0

Replace <owner>/<repo> with the GitHub repository slug of the upstream OrbitalReg release. A non-zero exit means either the image was not signed by this workflow or the cosign-installer / Rekor entry has been tampered with — either way, do not run the image.

To fetch the attached CycloneDX SBOM:

bash
cosign download attestation \
  ghcr.io/<owner>/orbitalreg-api:2026.1.0 \
  | jq -r '.payload | @base64d | fromjson | .predicate' \
  > orbitalreg-api-2026.1.0.cdx.json

The same workflow uploads the SBOMs as a 90-day workflow artefact for maintainer convenience; the canonical copies live in Rekor and can be re-derived from the attestation envelope above.

Local dry-run ​

Before tagging a release, a maintainer can sanity-check that the three build contexts still produce valid multi-arch images locally:

bash
make release-test

This runs docker buildx build --platform linux/amd64,linux/arm64 for each of the three Dockerfiles without pushing. Override RELEASE_VERSION and RELEASE_OWNER to mirror what GitHub Actions will publish:

bash
RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make release-test

make release-images is the same workflow with --push enabled, for the offline / vendored-mirror scenario where the GitHub Actions runner can't reach the target registry. The caller must be docker login-ed to the registry already; signing is not performed by this target — sign with the printed cosign sign commands separately.

Cutting a release ​

The full vendor-side workflow:

bash
# 1. Confirm the working tree compiles and the format looks right.
make version-check     # refuses non-CalVer current-tag describes
make api-build && cd frontend && npm run build && cd ..

# 2. Tag the new release. Stable: 2026.1.0. RC: 2026.1.0-rc.1.
git tag 2026.1.0
git push origin 2026.1.0

GitHub Actions takes over from there:

  1. release.yml triggers on the tag.
  2. Three multi-arch images get built, pushed, signed, and attested.
  3. The chart is packaged, signed, pushed to the GHCR OCI channel, and (if configured) mirrored to the static-index repo — see the Helm chart channel section below.
  4. The operator install manifest is generated, signed, attached to the GitHub Release, and (if configured) mirrored to a static-host repo — see the Operator install manifest channel section below.
  5. The air-gapped tarball is composed from the just-published image tags, the chart artefact, and the operator manifest artefact, signed, and attached to the GitHub Release — see the Air-gapped tarball channel section below.
  6. The workflow summary publishes verification snippets and the per-component digests.
  7. Phase E (License-issuer CLI) is a separate vendor-side workflow — see License-issuer CLI below — because per-customer envelopes are bound to a specific install UUID and the vendor signs them on-demand, outside the tagged release flow.

Once the workflow finishes, a maintainer can run the release checklist to confirm the chart version/appVersion, the three image tags, their cosign signatures, and their CycloneDX SBOM attestations all match the tag before announcing the release:

bash
RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make release-checklist

The chart check runs fully offline (it re-derives the packaged Chart.yaml locally, the same way chart-package-test does); the image/cosign/SBOM checks need docker/cosign plus registry access and report SKIP rather than a false FAIL when either is unavailable.

Trigger pattern reference ​

Tag shapeStable imagesAdvances :latest
2026.1.0yesyes
2026.1.0-rc.1yes (versioned tag only)no
2026.1.0-pre.4yes (versioned tag only)no
v0.5.1 (legacy SemVer)no — workflow ignoresno
cli/v1.2.3no — handled by cli-release.ymlno
provider/v1.0.0no — handled by terraform-provider-release.ymlno
backstage-plugin/v1.0.0no — handled by backstage-plugin-release.ymlno

The CalVer schema landed in item 69 / delivery #77 in 2026-05; pre-CalVer SemVer tags are intentionally a no-op so a historical-tag re-checkout can't accidentally republish a stale latest.

Permissions required ​

The workflow needs three GitHub Actions permission scopes:

ScopeUsed byWhy
contents: writerelease publishAttaches the SBOM workflow artefact, the operator install manifest, and the air-gapped tarball + signature + checksum to the GitHub Release.
packages: writedocker pushPushes to GHCR under the workflow's GitHub-Actions identity.
id-token: writecosign keylessMints the short-lived Fulcio certificate via OIDC token exchange.

No long-lived secrets are used in Phase A or the OCI half of Phase B. The optional static-index half of Phase B (charts.orbitalreg.com) introduces the first long-lived secret (CHARTS_REPO_TOKEN); Phase E (License-issuer) introduces the Ed25519 vendor signing key.

Helm chart channel (Phase B) ​

The chart at charts/orbitalreg ships through two parallel channels. Both publish the same .tgz; pick whichever fits the customer's existing tooling.

Channel B-1: OCI (default, no extra setup) ​

Always-on. The workflow runs helm push to oci://ghcr.io/<owner>/charts and signs the resulting OCI manifest with cosign sign keyless, matching the runtime-image flow. Customers consume it directly from GHCR — no separate hosted index.yaml required:

bash
helm install orbitalreg \
  oci://ghcr.io/<owner>/charts/orbitalreg \
  --version 2026.1.0 \
  --namespace orbitalreg --create-namespace \
  -f my-values.yaml

Verify the chart's provenance the same way as a runtime image:

bash
cosign verify \
  --certificate-identity-regexp 'https://github.com/<owner>/<repo>/' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/<owner>/charts/orbitalreg:2026.1.0

The packaged chart's Chart.yaml version + appVersion are rewritten to the CalVer tag at publish time, and its values.yaml default api.image.repository / frontend.image.repository are rewritten to point at the just-published ghcr.io/<owner>/orbitalreg-{api,frontend} images. A fresh helm install therefore Just Works against GHCR without an explicit --set api.image.repository=... override.

Channel B-2: Static index (opt-in mirror) ​

For customers whose tooling expects the classic helm repo add flow (GitOps controllers that don't speak OCI yet, internal artifact proxies, sales-enablement materials referencing a vanity domain), the workflow can additionally push the .tgz + cosign-signed blob + a regenerated index.yaml to a separate repository which Cloudflare Pages or GitHub Pages then serves at charts.orbitalreg.com.

This channel is opt-in. To enable, set both:

SettingWhereValue
CHARTS_REPO_NAMErepo variable<owner>/<chart-host-repo>
CHARTS_REPO_URLrepo variable (optional)base URL where the .tgz files are served (default https://charts.orbitalreg.com)
CHARTS_REPO_TOKENrepo secretPAT or fine-grained token with push rights to the chart-host repo

When configured, the workflow clones the chart-host repo, drops the new .tgz + .sig + .cert next to the existing entries, runs helm repo index . --url <CHARTS_REPO_URL> --merge index.yaml, and pushes a single commit per release. Customers then:

bash
helm repo add orbitalreg https://charts.orbitalreg.com
helm repo update
helm install orbitalreg orbitalreg/orbitalreg \
  --version 2026.1.0 \
  --namespace orbitalreg --create-namespace \
  -f my-values.yaml

Verify the .tgz blob with cosign before installing (the .cert + .sig live alongside the .tgz on the same host):

bash
curl -fsSL -o orbitalreg-2026.1.0.tgz      https://charts.orbitalreg.com/orbitalreg-2026.1.0.tgz
curl -fsSL -o orbitalreg-2026.1.0.tgz.sig  https://charts.orbitalreg.com/orbitalreg-2026.1.0.tgz.sig
curl -fsSL -o orbitalreg-2026.1.0.tgz.cert https://charts.orbitalreg.com/orbitalreg-2026.1.0.tgz.cert

cosign verify-blob \
  --certificate orbitalreg-2026.1.0.tgz.cert \
  --signature   orbitalreg-2026.1.0.tgz.sig \
  --certificate-identity-regexp 'https://github.com/<owner>/<repo>/' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  orbitalreg-2026.1.0.tgz

Local dry-run ​

Before tagging, a maintainer can sanity-check the packaged chart on their laptop using the same script the workflow invokes:

bash
RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make chart-package-test

Output: bin/orbitalreg-2026.1.0.tgz with the rewritten Chart.yaml

  • values.yaml. No push, no signing — re-run as often as needed.

Workflow artefact ​

Every release also uploads helm-chart-<version> as a 90-day GitHub Actions artefact (the .tgz + .sig + .cert), so a maintainer can download the published bundle without reaching into GHCR.

Operator install manifest channel (Phase C) ​

Customers who run the OrbitalReg Kubernetes operator (Item 23 / Item 68 Channel 3) can land the entire control plane in one kubectl apply call. The release workflow bakes a single multi-document YAML — namespace + 7 CRDs + ServiceAccount + ClusterRole + ClusterRoleBinding

  • manager Deployment — with the Deployment's image reference rewritten to the just-published ghcr.io/<owner>/orbitalreg-operator:<version>, signs it with cosign keyless, and ships it through two parallel channels.

Channel C-1: GitHub Release asset (default, no extra setup) ​

Always-on. The workflow uploads three files to the auto-created GitHub Release for every CalVer tag:

AssetPurpose
orbitalreg-operator-<version>-install.yamlThe kubectl-applyable bundle (namespace + CRDs + RBAC + Deployment).
orbitalreg-operator-<version>-install.yaml.sigCosign keyless signature for the bundle.
orbitalreg-operator-<version>-install.yaml.certFulcio certificate that minted the signature.

Customer flow:

bash
# 1. Verify the bundle (recommended) before applying.
VER=2026.1.0
REPO=<owner>/<repo>

curl -fsSL -o install.yaml      "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-operator-${VER}-install.yaml"
curl -fsSL -o install.yaml.sig  "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-operator-${VER}-install.yaml.sig"
curl -fsSL -o install.yaml.cert "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-operator-${VER}-install.yaml.cert"

cosign verify-blob \
  --certificate install.yaml.cert \
  --signature   install.yaml.sig \
  --certificate-identity-regexp "https://github.com/${REPO}/" \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  install.yaml

# 2. Apply.
kubectl apply -f install.yaml

The bundle includes the orbitalreg-operator Namespace declaration ahead of any namespaced resource so a single kubectl apply against a fresh cluster lands every kind in one shot — no two-step "create-namespace then apply" dance, no waiting for retries.

After the operator Pod boots in orbitalreg-operator, the customer provisions the orbitalreg-credentials Secret (with endpoint + token keys) and starts authoring OrbitalRegProject / OrbitalRegRepository / OrbitalRegServiceAccount etc. CRs against the freshly-installed CRDs.

Channel C-2: Static host (opt-in mirror) ​

For customers whose tooling expects a stable URL — GitOps controllers that pull manifests at apply time, sales-enablement materials referencing a vanity domain, internal manifest mirrors — the workflow can additionally push the bundle to a separate repository which Cloudflare Pages or GitHub Pages then serves at operators.orbitalreg.com.

This channel is opt-in. To enable, set both:

SettingWhereValue
OPERATOR_MANIFEST_REPO_NAMErepo variable<owner>/<manifest-host-repo>
OPERATOR_MANIFEST_REPO_TOKENrepo secretPAT or fine-grained token with push rights to the manifest-host repo

When configured, the workflow drops the bundle under two paths in the host repo:

PathUpdated by
<version>/install.yaml (+ .sig + .cert)every CalVer tag (stable + pre-release)
latest/install.yaml (+ .sig + .cert)stable releases only — pre-release tags do not advance latest

Customers then pin to a CalVer tag (recommended for change control) or the rolling latest (continuous-delivery shops):

bash
# Pinned to a specific release.
kubectl apply -f https://operators.orbitalreg.com/2026.1.0/install.yaml

# Rolling — always the latest stable release.
kubectl apply -f https://operators.orbitalreg.com/latest/install.yaml

Verify with the same cosign verify-blob flow as the GitHub Release channel — the .sig + .cert live alongside install.yaml at both the versioned and latest/ paths.

Local dry-run ​

Before tagging, a maintainer can sanity-check the generated bundle on their laptop using the same script the workflow invokes:

bash
RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make operator-manifest-test

Output: bin/orbitalreg-operator-2026.1.0-install.yaml with the operator-image reference rewritten to ghcr.io/acme/orbitalreg-operator:2026.1.0. No push, no signing — re-run as often as needed.

The same script is also reachable from inside tools/k8s-operator/ via the kubebuilder-style target the operator's own Makefile exposes:

bash
cd tools/k8s-operator
RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make generate-install-manifest
# Output: tools/k8s-operator/dist/orbitalreg-operator-2026.1.0-install.yaml

Workflow artefact ​

Every release also uploads operator-install-<version> as a 90-day GitHub Actions artefact (the .yaml + .sig + .cert), so a maintainer can download the published bundle without reaching into the Release page.

What the bundle is not ​

  • Not a Helm chart: customers running the operator via Helm should use the chart at charts/orbitalreg-operator/ (not the same as the platform chart at charts/orbitalreg/ — that one bundles the API + frontend, not the operator). The install manifest is the no-Helm-dependency alternative for plain kubectl apply shops.
  • Not a webhook bundle: validating-admission webhooks (Item 28 Phase E) are not included in the install manifest because they require a TLS material provisioning story (cert-manager Certificate or pre-created Secret) that varies per cluster. The Helm chart templates them; the manifest stays minimal so a copy-paste install doesn't strand a half-configured webhook.
  • Not customer config: the manifest installs the operator binary + RBAC + CRDs only. The customer still creates orbitalreg-credentials Secret + their first OrbitalRegProject CR after kubectl apply returns.

Air-gapped tarball channel (Phase D) ​

Banks, defence customers, and regulated healthcare deployments typically run Kubernetes clusters with no outbound internet — the clusters can't reach GHCR for image pulls, can't reach charts.orbitalreg.com for the Helm index, and can't reach Sigstore to verify signatures inline. For those customers the release workflow composes a single self-contained tarball that ships everything needed to install OrbitalReg, signs the outer blob with cosign keyless, and attaches the four assets to the GitHub Release.

What's inside ​

The tarball unpacks into orbitalreg-<version>-airgapped/ with this shape:

PathContents
images/orbitalreg-{api,frontend,operator}-<version>.tar.gz`docker save
charts/orbitalreg-<version>.tgzSame Helm chart Phase B publishes — Chart.yaml + values.yaml already rewritten to point at ghcr.io/<owner>/orbitalreg-{api,frontend} so a customer's local registry mirror only has to honour the same image paths.
operator/orbitalreg-operator-<version>-install.yamlSame single-file kubectl-applyable manifest Phase C publishes, with the operator image path rewritten to ghcr.io/<owner>/orbitalreg-operator:<version>.
docs/Static HTML render of the docs site — point a web server at the directory to serve internal documentation behind the customer's firewall.
licenses/oss-licenses.txtConcatenated syft -o text output across the three runtime images. Maps every shipped binary to its OSS license — required by procurement at most regulated buyers.
README.mdCustomer-side install runbook (also reproduced below).
manifest.jsonMachine-readable metadata: version, generated-at timestamp, component image refs, which optional steps were skipped at build time (always all-zero on the production CI build).
checksums.sha256sha256 of every other file in the bundle. Customers run sha256sum -c checksums.sha256 after unpacking to detect transit corruption.

The outer tarball ships with three sibling files attached to the same GitHub Release:

AssetPurpose
orbitalreg-<version>-airgapped.tar.gzThe bundle itself.
orbitalreg-<version>-airgapped.tar.gz.sigCosign keyless signature for the outer blob.
orbitalreg-<version>-airgapped.tar.gz.certFulcio certificate that minted the signature.
orbitalreg-<version>-airgapped.tar.gz.sha256Single-line sha256 of the outer tarball. Lets a customer pin the exact byte-stream they downloaded before transferring it onto the air-gapped network.

Customer flow ​

bash
VER=2026.1.0
REPO=<owner>/<repo>

# 1. Download all four assets on a host with internet access.
curl -fsSL -o bundle.tar.gz       "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-${VER}-airgapped.tar.gz"
curl -fsSL -o bundle.tar.gz.sig   "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-${VER}-airgapped.tar.gz.sig"
curl -fsSL -o bundle.tar.gz.cert  "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-${VER}-airgapped.tar.gz.cert"
curl -fsSL -o bundle.tar.gz.sha256 "https://github.com/${REPO}/releases/download/${VER}/orbitalreg-${VER}-airgapped.tar.gz.sha256"

# 2. Verify the cosign-keyless signature (also doable air-gapped if the
#    customer carries a frozen Sigstore root + Rekor pubkey).
cosign verify-blob \
  --certificate bundle.tar.gz.cert \
  --signature   bundle.tar.gz.sig \
  --certificate-identity-regexp "https://github.com/${REPO}/" \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  bundle.tar.gz

# 3. Confirm transit integrity.
sha256sum -c bundle.tar.gz.sha256

# 4. Transfer onto the air-gapped network (USB / write-once media / DMZ
#    SCP — whatever the customer's transfer policy mandates) and unpack.
tar xzf bundle.tar.gz
cd orbitalreg-${VER}-airgapped/

# 5. Confirm inner integrity.
sha256sum -c checksums.sha256

# 6. Load runtime images into the local mirror or directly into the
#    cluster's container runtime.
for img in images/*.tar.gz; do
  gunzip -c "$img" | docker load
done

# 7. Install. Pick whichever flavour matches the cluster's tooling:

# 7a. Helm install (recommended for greenfield clusters):
helm install orbitalreg charts/orbitalreg-${VER}.tgz \
  --namespace orbitalreg --create-namespace \
  -f my-values.yaml

# 7b. Operator install (recommended for GitOps shops):
kubectl apply -f operator/orbitalreg-operator-${VER}-install.yaml

After install, the customer applies the per-deployment license envelope issued separately by the vendor's license-issuer CLI (Phase E of the release pipeline) — the bundle deliberately doesn't include a generic envelope, because each envelope is bound to the target install's UUID.

Local dry-run ​

The same script the workflow invokes is also wired into the top-level Makefile, with every "needs-internet" step skipped by default so a maintainer can sanity-check the bundle's shape on their laptop without a docker daemon, npm cache, syft binary, or Sigstore round-trip:

bash
RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make airgapped-bundle-test

Output: bin/orbitalreg-2026.1.0-airgapped.tar.gz plus a .sha256 sibling. The bundle contains the chart + operator manifest + a README + manifest.json + placeholder image inventory + license-hint text — enough to validate path layout and the README rendering, even though the production bundle additionally carries the three image tarballs (~600–800 MB combined) and the rendered docs HTML.

To exercise individual production-only steps, override the corresponding SKIP_* env var:

bash
# Pull + save real images (requires docker + ghcr.io login):
SKIP_IMAGES=0 RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make airgapped-bundle-test

# Build the docs (requires npm):
SKIP_DOCS=0 RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make airgapped-bundle-test

# Generate real syft-derived licenses (requires syft + image pull access):
SKIP_LICENSES=0 RELEASE_VERSION=2026.1.0 RELEASE_OWNER=acme make airgapped-bundle-test

Workflow artefact ​

Every release also uploads airgapped-<version> as a 30-day GitHub Actions artefact (the .tar.gz + .sig + .cert + .sha256), so a maintainer can download the published bundle without reaching into the Release page. The retention is shorter than the Phase A/B/C artefacts (90 days) because the bundle is several hundred MB and the canonical copy lives on the Release tag itself — no benefit to keeping two long-lived copies.

What the bundle is not ​

  • Not a customer-bound license: the per-customer envelope is issued separately via the vendor's license-issuer CLI (Phase E) and applied to the OrbitalReg admin page after install. Same envelope shape, same Ed25519 signing key — just not in this tarball, because envelopes are install-UUID-scoped.
  • Not a Sigstore root: customers operating fully air-gapped have to carry a frozen Sigstore root + Rekor public key separately if they want to verify the outer cosign signature without internet. The alternative is verifying on an internet-connected DMZ host before transfer onto the air-gapped network.
  • Not encrypted at rest: hyper-sensitive customers who want a customer-public-key-encrypted variant should request that as a follow-up — adding an encryption pass to the release workflow is a separate item (the bundle would carry a per-recipient .enc instead of the plain .tar.gz).
  • Not a Kubernetes cluster: the bundle assumes a working cluster with admin RBAC plus a local image mirror or registry the cluster trusts. Bare-metal cluster bring-up is out of scope.

License-issuer CLI (Phase E) ​

The vendor signs a per-customer license envelope outside the tagged release flow — every envelope is bound to one specific install UUID (the value the customer reads off /admin/license), so a "release" of envelopes does not exist; one is signed each time a customer requests or renews. The signing tool ships under tools/license-issuer/ as its own Go module so the release-engineer host doesn't need the api server's dependency tree.

Build ​

bash
make license-issuer-vendor
# → bin/license-issuer

The standalone tool also builds via cd tools/license-issuer && make build inside its own module, useful when shipping the binary alone to a hardware-key host that never sees the OrbitalReg source.

Sign a customer envelope ​

bash
./bin/license-issuer sign \
    --install-id "abc-123-def-456" \
    --customer "ACME GmbH" \
    --customer-email "ops@acme.example" \
    --kind commercial \
    --valid-from 2026-05-15 \
    --valid-until 2027-05-14 \
    --signing-key "$VENDOR_PRIVATE_KEY_PATH" \
    > acme-license-2026.json

The output is a JSON document with two fields:

json
{
  "envelope": "ORBREG-LIC-...",
  "summary": { "customer": "ACME GmbH", "install_id": "...", "kind": "commercial", ... }
}

The customer pastes the envelope into /admin/license → "Apply license", or the operator can pipe it directly:

bash
jq -r .envelope acme-license-2026.json | \
    curl -X PUT --data-binary @- -H "Content-Type: text/plain" \
        https://orbitalreg.example/api/admin/license

Sign an internal license (long-lived) ​

bash
./bin/license-issuer sign-internal \
    --install-id "internal-demo-1" \
    --customer "OrbitalReg-Demo" \
    --valid-until 2031-01-01 \
    --signing-key "$INTERNAL_KEY"

sign-internal defaults to a 5-year validity window (vs. 1 year on sign) and fixes kind=internal, which the runtime treats as a Free-Forever-Tier license — used for investor demos, long-running internal-staging instances, and partner sandboxes that should not expire mid-quarter.

Signing-key precedence ​

The CLI resolves a signing key in this order:

  1. --signing-key <path> — raw 32 bytes or 64 hex chars
  2. --seed-hex <64-hex>
  3. $ORBITALREG_LICENSE_SEED_HEX
  4. In-repo dev seed — only when --allow-dev-key is set, never for customer-facing envelopes. The dev seed produces an envelope that only verifies on a binary built with the matching dev public key (the in-repo default at api/internal/license/pubkey.go, derived from a 32-byte zero seed).

After every successful sign, stderr reports the resolved signing_source (file, hex-flag, env:ORBITALREG_LICENSE_SEED_HEX, or dev-seed (in-repo)) plus the hex-encoded signing_pubkey — compare it against the PublicKeyHex the customer's binary embeds before sending the envelope; drift means the envelope will be rejected at paste time.

Why not bake into the tagged-release workflow? ​

Customer envelopes are not a release artefact. Each one is bound to exactly one install UUID and exactly one customer/contact pair, so there is no "build N envelopes per tag" loop — there is only "issue one envelope, on demand, when the customer asks." The signing key is the only long-lived secret in the entire release chain, so keeping it out of CI was a deliberate choice: the vendor signs from a hardware key on a vetted release-engineer host, never from a runner with a public OIDC identity.

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