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.

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.

Released under the Apache-2.0 License.