Skip to content
Talk to our solutions team

Harbor

Harbor (internal codename Mitra) is kis.ai’s secure binary-distribution service. It serves signed artifacts to licensed customers and a public bootstrap client (kvm). This guide covers deploying the server, generating and revoking licenses, publishing artifacts, and using the kvm client.

TermMeaning
BucketA namespace of artifacts (baas, ai, dev, forge, ops, tools). _kvm is the internal, public bootstrap bucket. The authoritative list is buckets.yaml, embedded into every binary at compile time.
Wildcard bucketall — an entitlement that grants access to every bucket.
BundleA named group of artifacts installable in one shot (kis-cli, baas-core, ai-full, …). See kvm bundles.
ArtifactOne binary for one bucket/name/version/platform, stored with a SHA-256 checksum and an Ed25519 signature.
LicenseA signed JSON grant: org identity, entitled buckets, validity window. A bearer token — anyone holding it can download entitled artifacts until it expires or is revoked.
EntitlementA bucket name (or all) the license grants.
Signing keyAn Ed25519 key pair. The private key signs licenses + artifacts; the public key is embedded in kvm and used by the server to validate licenses.
BinaryRoleRuns where
harbor.svc (cmd/harbor-server)Public distribution server (read-only over storage)Internet-facing host
harbor-admin (cmd/harbor-admin)Sign licenses, sign + publish artifacts, prune storageSigning host (holds the private key)
kvm (cmd/kvm)Customer-side installer / updater (the “kis.ai version manager”)Customer machines

Separation of duties: harbor.svc needs only the public key. Keep the private key (harbor.key) off the serving host — run harbor-admin on a separate, restricted signing host and publish already-signed artifacts.

Build tasks live in build.yaml and run via kis flow:

Terminal window
kis flow -t build # -> dist/harbor.svc and dist/harbor-admin

Or directly with Go:

Terminal window
go build -o dist/harbor.svc ./cmd/harbor-server
go build -o dist/harbor-admin ./cmd/harbor-admin
Terminal window
kis flow -t keygen # -> ./keys/harbor.key (private), ./keys/harbor.pub (public)
# or:
./dist/harbor-admin keygen --output ./keys
  • harbor.keyprivate, signs licenses and artifacts. Guard it like a root CA key; ideally keep it in openbao / an HSM and only on the signing host.
  • harbor.pubpublic, embedded into kvm at build time and used by the server to validate licenses.

Rotating this key today is a flag day: it is embedded in every deployed kvm. Plan key custody up front. See the design note.

Every setting can come from the config file or a serve flag; an explicit flag always overrides the config value (handy for the systemd unit).

Config file/etc/harbor/config.yaml (or ./config.yaml, or --config <file>). See the example server.yaml:

server:
addr: ":8443"
public_key: /etc/harbor/keys/harbor.pub
revocation_list: /etc/harbor/keys/revoked.txt # optional
# tls_cert: /etc/harbor/tls/server.crt
# tls_key: /etc/harbor/tls/server.key
storage:
backend: local # local | s3
local:
path: /var/harbor/artifacts
s3: # used when backend: s3
bucket: kis-harbor
region: us-east-1
use_ssl: true
# endpoint: nyc3.digitaloceanspaces.com # omit for AWS S3
# access_key_id: ...
# secret_access_key: ...

serve flags (each overrides the matching config key):

FlagConfig keyDefaultPurpose
--addrserver.addr:8443Listen address
--public-keyserver.public_keyEd25519 public key for license validation (required)
--storage-backendstorage.backendlocallocal or s3
--storage-pathstorage.local.path/var/harbor/artifactsLocal artifact directory
--tls-cert / --tls-keyserver.tls_cert / server.tls_keyEnable HTTPS when both set
--revocation-listserver.revocation_listDenylist of revoked licenses
--configExplicit config-file path

S3 credentials (storage.s3.*) are config-only — there are no flags for them.

Terminal window
# Local storage, HTTPS:
./dist/harbor.svc serve \
--addr :8443 \
--public-key /etc/harbor/keys/harbor.pub \
--storage-path /var/harbor/artifacts \
--tls-cert /etc/harbor/tls/server.crt \
--tls-key /etc/harbor/tls/server.key \
--revocation-list /etc/harbor/keys/revoked.txt
# S3 backend (S3 creds come from config.yaml / storage.s3.*):
./dist/harbor.svc serve --addr :8443 --storage-backend s3 \
--public-key /etc/harbor/keys/harbor.pub

The server handles SIGINT/SIGTERM with a graceful 30s drain, so in-flight downloads finish on deploy/restart.

/etc/systemd/system/harbor.service
[Unit]
Description=kis.ai Harbor distribution server
After=network-online.target
Wants=network-online.target
[Service]
User=harbor
Group=harbor
ExecStart=/home/harbor/harbor.svc serve \
--addr :8443 \
--public-key /etc/harbor/keys/harbor.pub \
--storage-path /var/harbor/artifacts \
--tls-cert /etc/harbor/tls/server.crt \
--tls-key /etc/harbor/tls/server.key \
--revocation-list /etc/harbor/keys/revoked.txt
Restart=on-failure
RestartSec=2
# Hardening: the serving host should NOT hold the private key.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/harbor
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now harbor
curl -fsS https://your-host:8443/health # {"status":"healthy",...}

Revocation reload: the denylist is read at startup. After editing it, sudo systemctl restart harbor to apply.

Run harbor-admin on the signing host (it needs harbor.key). For a local backend the storage path must be the same directory the server reads (or a synced copy); for S3 it writes to the same bucket.

Terminal window
./harbor-admin artifact upload ./iam.svc \
--bucket baas --name iam.svc --version 1.2.0 \
--platform linux-amd64 \
--private-key ./keys/harbor.key \
--storage-path /var/harbor/artifacts

--platform defaults to the host’s GOOS-GOARCH. The command computes the SHA-256, signs it, and writes artifact.bin + meta.json.

For a directory laid out as <bucket>/<binary>:

Terminal window
./harbor-admin artifact sync ./build/1.2.0/linux-amd64 \
--version 1.2.0 --platform linux-amd64 \
--private-key ./keys/harbor.key \
--storage-path /var/harbor/artifacts
# --dry-run to preview, --force to overwrite existing

Only buckets present in the embedded registry are accepted; unknown bucket directories are skipped (and internal buckets like _kvm are ignored).

Terminal window
kis flow -t build-kvm # builds bin/kvm-* for all platforms WITH the embedded key
kis flow -t release-kvm # uploads them to the _kvm bucket (automate.yaml)
Terminal window
./harbor-admin artifact list baas --storage-path /var/harbor/artifacts
./harbor-admin artifact delete --bucket baas --name iam.svc --version 1.1.0 --platform linux-amd64 --storage-path /var/harbor/artifacts
./harbor-admin artifact delete-version --version 1.1.0 --storage-path /var/harbor/artifacts # --dry-run supported
./harbor-admin artifact cleanup --storage-path /var/harbor/artifacts # remove empty/undocumented dirs

upload/sync write a control.zsync automatically, but artifacts published before delta support have none — clients silently fall back to full downloads for them. reindex walks storage and generates any missing control blobs so delta applies retroactively (no re-upload needed):

Terminal window
./harbor-admin artifact reindex --storage-path /var/harbor/artifacts # --dry-run to preview
./harbor-admin artifact reindex --storage-path /var/harbor/artifacts --force # regenerate all
Terminal window
./harbor-admin license create \
--org-id "acme" \
--org-name "Acme Corp" \
--entitlements "baas,ai" \
--duration "365d" \
--private-key ./keys/harbor.key \
--output ./acme-license.json
FlagNotes
--org-id / --org-nameCustomer identity (org-id is also a revocation handle). Required.
--entitlementsComma-separated buckets, or all. Valid: ai, baas, dev, forge, ops, tools, all (internal _kvm is not allowed). Required.
--duration30d, 12d, 1y (= 365d), or any Go duration (720h). Default 365d.
--private-keySigning key. Required.
--outputWrite JSON here (mode 0600). Omit to print to stdout. Also prints a base64 form for kvm license activate.
Terminal window
./harbor-admin license verify ./acme-license.json --public-key ./keys/harbor.pub
./harbor-admin license info ./acme-license.json
{
"key": "KIS-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"org_id": "acme",
"org_name": "Acme Corp",
"entitlements": ["baas", "ai"],
"issued_at": "2026-06-18T00:00:00Z",
"expires_at": "2027-06-18T00:00:00Z",
"not_before": "2026-06-18T00:00:00Z",
"signature": "base64-ed25519-over-the-payload"
}

Deliver acme-license.json (or its base64 form) to the customer. They run kvm license activate <file-or-key>.

Licenses are bearer tokens validated by signature + time window, so a leaked license works until it expires — unless it is on the server’s revocation denylist.

A UTF-8 text file, one entry per line. An entry is either a license key (License.key) or an org-id (License.org_id). Blank lines and lines beginning with # are ignored; surrounding whitespace is trimmed.

/etc/harbor/keys/revoked.txt
# Revoked individual licenses (by license key):
KIS-AB12CD-EF34GH-IJ56KL-MN78OP
KIS-ZZ99YY-XX88WW-VV77UU-TT66SS
# Revoke EVERY license issued to an org (by org-id):
acme
  • Matching is exact against key and org_id.
  • Listing an org-id revokes all current and future licenses for that org — useful for offboarding a customer wholesale.
  • A revoked request is refused with HTTP 403 {"error":"license revoked"} and logged server-side (revoked license refused).
Terminal window
# 1. Add the key or org-id to the file referenced by --revocation-list
echo 'KIS-AB12CD-EF34GH-IJ56KL-MN78OP' >> /etc/harbor/keys/revoked.txt
# 2. Reload (the list is read at startup)
sudo systemctl restart harbor

The list is currently loaded once at startup. Hot reload (SIGHUP) and a DB-backed store are on the roadmap — see the design note. Until then, revoking requires a restart (graceful, so no downloads are dropped).

5.1 Bootstrap install (public, no license)

Section titled “5.1 Bootstrap install (public, no license)”
Terminal window
curl -sSL https://get.kisai.dev/install.sh | sh
export PATH="$HOME/.kisai/bin:$PATH" # follow the printed instruction

This downloads kvm for your platform into ~/.kisai/bin. (Override the source with HARBOR_URL=https://your-host:8443.)

Terminal window
kvm license activate ./acme-license.json # file, or the base64 string
kvm license status

The license is stored at ~/.kisai/license.json (mode 0600). The server URL lives in ~/.kisai/config.yaml (harbor.server, default https://get.kisai.dev).

Terminal window
kvm install kis-cli # a bundle (group of binaries)
kvm install baas # every artifact in the baas bucket (entitled)
kvm install baas/iam.svc # one specific artifact
kvm install iam.svc # find by name across entitled buckets
kvm install all # everything entitled
kvm install baas -v 1.2.0 # pin a version
sudo kvm install baas --system # install to /usr/local/bin instead of ~/.kisai/bin

No wasted downloads. Before fetching, kvm compares the local binary’s SHA-256 against the remote artifact’s. If they match it prints up to date and transfers nothing — so re-running kvm install kis-cli when everything is current downloads zero bytes. When a binary has changed, kvm delta-updates it: only the differing byte ranges are pulled (rsync-style, over HTTP Range) and the rest is reused from the local copy, then the rebuilt binary is checksum+signature verified. You’ll see e.g. ✓ 1.2.1 (delta: 256 KB of 64 MB). Pass --force to disable both and re-download in full.

Bundles and buckets download their artifacts in parallel (up to 4 at a time), so kvm install kis-cli fetches its binaries concurrently rather than one after another.

Browse what’s available:

Terminal window
kvm buckets --entitled
kvm bundles --detailed
kvm list --remote --full
Terminal window
kvm update # update all installed components to latest
kvm update baas # only the baas bucket
kvm update --force # re-download everything, even if unchanged
kvm list # installed components with their versions

update walks every installed binary, and components already matching the latest are reported as up to date with no download (same SHA-256 check as install). Installed versions are tracked in ~/.kisai/state.json. update and self-update refuse to move to an older version than what’s installed (anti-rollback) — protection against a compromised mirror serving a stale, known-vulnerable but validly-signed build. To override deliberately:

Terminal window
kvm update --allow-downgrade
kvm self-update --allow-downgrade
Terminal window
kvm self-update # no-op if already current (prints "already up to date")
kvm self-update --force # re-download even if unchanged

self-update uses the public bootstrap endpoint (no license needed). It compares the running binary’s SHA-256 against the published one and skips the download when they match; it also refuses an older version (--allow-downgrade to override). It does not use delta — kvm is small enough that the skip is the only optimization worth having.

Terminal window
kvm uninstall iam.svc
kvm uninstall kis-cli # a whole bundle
kvm uninstall --all # remove every component, config, and kvm

When an artifact is published, harbor-admin computes a small control blob (control.zsync) — the rolling + strong checksum of each fixed-size block of the new binary — and stores it next to the artifact. To update, kvm:

  1. Fetches the control blob (…/control).
  2. Scans its local (old) binary with a rolling checksum to find which of the new file’s blocks it already has — even if they’ve shifted position.
  3. Fetches only the missing byte ranges via HTTP Range requests.
  4. Reassembles the new binary from local + fetched bytes and verifies the full SHA-256 and signature before installing.

Because the server only serves static block checksums and byte ranges, there is no per-request server computation — it stays cache/CDN-friendly. If anything is off (no control blob, checksum mismatch, bad signature), kvm silently falls back to a full, fail-closed download. The reconstructed binary can never be corrupt or unsigned.

  • Signed artifacts. Each artifact is signed with Ed25519 over its SHA-256 digest. harbor-admin signs at publish time; the server serves the signature in X-Signature and the digest in X-Checksum-SHA256.
  • Embedded trust anchor. The public key is compiled into kvm (kis flow -t build-kvm, build tag embed_key). Verification therefore holds even if TLS is compromised or a mirror is malicious.
  • Fail-closed verification. kvm refuses any download whose signature or checksum is missing/invalid, or when no trusted key is available — it does not silently fall back to “checksum only”. A tampered binary, a stripped X-Signature header, and a wrong signature are all rejected.
  • Anti-rollback. Downgrades are refused unless --allow-downgrade.
  • License validation. The server checks signature, not_before/expires_at, the entitlement for the requested bucket, and the revocation denylist.
  • Transport. Always run the server behind TLS in production; the embedded key is defense-in-depth, not a substitute for HTTPS.
MethodEndpointDescription
GET/healthHealth check
GET/install.shBootstrap installer script
GET/kvm/{platform}Download the kvm binary
GET/kvm/{platform}/metakvm metadata (version, checksum, signature)

Protected (license required — Authorization: Bearer <base64-license>)

Section titled “Protected (license required — Authorization: Bearer <base64-license>)”
MethodEndpointDescription
GET/v1/bucketsList entitled buckets
GET/v1/artifacts/{bucket}List artifacts
GET/v1/artifacts/{bucket}/{name}List versions
GET/v1/artifacts/{bucket}/{name}/{version}/{platform}Download (supports Range)
GET/v1/artifacts/{bucket}/{name}/{version}/{platform}/metaArtifact metadata
GET/v1/artifacts/{bucket}/{name}/{version}/{platform}/controlzsync delta control blob
GET/v1/artifacts/{bucket}/{name}/latest/{platform}Latest version metadata

Responses set X-Checksum-SHA256 and X-Signature on downloads. They also set an ETag (the SHA-256) and Cache-Control, so a conditional GET with If-None-Match returns 304 Not Modified when the client already has that exact artifact — no body transferred. Licensed endpoints are marked private (a shared CDN must not cache them, since that would bypass the license check); only the public /kvm/{platform} endpoint is public.

Both the local and S3 backends use the same key structure:

<storage-root>/
<bucket>/<name>/<version>/<platform>/
artifact.bin # the binary
meta.json # {bucket,name,version,platform,size,checksum,signature,created_at}
control.zsync # per-block checksums for delta downloads (written at publish)

Example: baas/iam.svc/1.2.0/linux-amd64/artifact.bin.

control.zsync is generated automatically by harbor-admin upload/sync. Artifacts published before this feature simply have no control blob — clients detect its absence and fall back to a full download. Run harbor-admin artifact reindex (§2.5) to backfill them.

SymptomLikely cause / fix
kvm: signature verification required but not possibleThe build has no embedded key and no ~/.kisai/harbor.pub, or the server didn’t send X-Signature/X-Checksum-SHA256. Use a release kvm (kis flow -t build-kvm); for dev, drop harbor.pub into ~/.kisai/.
kvm: signature verification failedThe binary doesn’t match its signature — tampering or a key mismatch between the signing key and the embedded public key.
kvm update: server offers older … skippingAnti-rollback. The server’s “latest” is older than what you have. Investigate the server; override with --allow-downgrade only if intentional.
Server: 403 license revokedThe license key or org-id is on --revocation-list. Expected for offboarded customers.
Server: 403 bucket not entitledThe license lacks that bucket; reissue with the right --entitlements.
Server: public key path is requiredPass --public-key or set server.public_key in config.
Server won’t start on :8443Port in use or missing TLS files; check --addr, --tls-cert, --tls-key.

See also: the quick-reference README and the distribution-hardening design note (security review & roadmap).