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.
Concepts
Section titled “Concepts”| Term | Meaning |
|---|---|
| Bucket | A 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 bucket | all — an entitlement that grants access to every bucket. |
| Bundle | A named group of artifacts installable in one shot (kis-cli, baas-core, ai-full, …). See kvm bundles. |
| Artifact | One binary for one bucket/name/version/platform, stored with a SHA-256 checksum and an Ed25519 signature. |
| License | A 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. |
| Entitlement | A bucket name (or all) the license grants. |
| Signing key | An Ed25519 key pair. The private key signs licenses + artifacts; the public key is embedded in kvm and used by the server to validate licenses. |
Components
Section titled “Components”| Binary | Role | Runs 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 storage | Signing host (holds the private key) |
kvm (cmd/kvm) | Customer-side installer / updater (the “kis.ai version manager”) | Customer machines |
Separation of duties:
harbor.svcneeds only the public key. Keep the private key (harbor.key) off the serving host — runharbor-adminon a separate, restricted signing host and publish already-signed artifacts.
1. Deploy the server
Section titled “1. Deploy the server”1.1 Build
Section titled “1.1 Build”Build tasks live in build.yaml and run via kis flow:
kis flow -t build # -> dist/harbor.svc and dist/harbor-adminOr directly with Go:
go build -o dist/harbor.svc ./cmd/harbor-servergo build -o dist/harbor-admin ./cmd/harbor-admin1.2 Generate signing keys (once)
Section titled “1.2 Generate signing keys (once)”kis flow -t keygen # -> ./keys/harbor.key (private), ./keys/harbor.pub (public)# or:./dist/harbor-admin keygen --output ./keysharbor.key— private, 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.pub— public, embedded intokvmat 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.
1.3 Configure
Section titled “1.3 Configure”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.keystorage: 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):
| Flag | Config key | Default | Purpose |
|---|---|---|---|
--addr | server.addr | :8443 | Listen address |
--public-key | server.public_key | — | Ed25519 public key for license validation (required) |
--storage-backend | storage.backend | local | local or s3 |
--storage-path | storage.local.path | /var/harbor/artifacts | Local artifact directory |
--tls-cert / --tls-key | server.tls_cert / server.tls_key | — | Enable HTTPS when both set |
--revocation-list | server.revocation_list | — | Denylist of revoked licenses |
--config | — | — | Explicit config-file path |
S3 credentials (
storage.s3.*) are config-only — there are no flags for them.
1.4 Run
Section titled “1.4 Run”# 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.pubThe server handles SIGINT/SIGTERM with a graceful 30s drain, so in-flight
downloads finish on deploy/restart.
1.5 systemd unit
Section titled “1.5 systemd unit”[Unit]Description=kis.ai Harbor distribution serverAfter=network-online.targetWants=network-online.target
[Service]User=harborGroup=harborExecStart=/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.txtRestart=on-failureRestartSec=2# Hardening: the serving host should NOT hold the private key.NoNewPrivileges=trueProtectSystem=strictReadWritePaths=/var/harbor
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now harborcurl -fsS https://your-host:8443/health # {"status":"healthy",...}Revocation reload: the denylist is read at startup. After editing it,
sudo systemctl restart harborto apply.
2. Publish artifacts
Section titled “2. Publish artifacts”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.
2.1 Upload a single artifact
Section titled “2.1 Upload a single artifact”./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.
2.2 Sync a build tree
Section titled “2.2 Sync a build tree”For a directory laid out as <bucket>/<binary>:
./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 existingOnly buckets present in the embedded registry are accepted; unknown bucket
directories are skipped (and internal buckets like _kvm are ignored).
2.3 Publish the kvm client
Section titled “2.3 Publish the kvm client”kis flow -t build-kvm # builds bin/kvm-* for all platforms WITH the embedded keykis flow -t release-kvm # uploads them to the _kvm bucket (automate.yaml)2.4 Manage / prune
Section titled “2.4 Manage / prune”./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 dirs2.5 Backfill delta control blobs
Section titled “2.5 Backfill delta control blobs”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):
./harbor-admin artifact reindex --storage-path /var/harbor/artifacts # --dry-run to preview./harbor-admin artifact reindex --storage-path /var/harbor/artifacts --force # regenerate all3. Generate & manage licenses
Section titled “3. Generate & manage licenses”3.1 Create
Section titled “3.1 Create”./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| Flag | Notes |
|---|---|
--org-id / --org-name | Customer identity (org-id is also a revocation handle). Required. |
--entitlements | Comma-separated buckets, or all. Valid: ai, baas, dev, forge, ops, tools, all (internal _kvm is not allowed). Required. |
--duration | 30d, 12d, 1y (= 365d), or any Go duration (720h). Default 365d. |
--private-key | Signing key. Required. |
--output | Write JSON here (mode 0600). Omit to print to stdout. Also prints a base64 form for kvm license activate. |
3.2 Verify / inspect
Section titled “3.2 Verify / inspect”./harbor-admin license verify ./acme-license.json --public-key ./keys/harbor.pub./harbor-admin license info ./acme-license.json3.3 License JSON
Section titled “3.3 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>.
4. Revoke licenses
Section titled “4. Revoke licenses”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.
4.1 Revocation-list format
Section titled “4.1 Revocation-list format”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.
# Revoked individual licenses (by license key):KIS-AB12CD-EF34GH-IJ56KL-MN78OPKIS-ZZ99YY-XX88WW-VV77UU-TT66SS
# Revoke EVERY license issued to an org (by org-id):acme- Matching is exact against
keyandorg_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).
4.2 Apply
Section titled “4.2 Apply”# 1. Add the key or org-id to the file referenced by --revocation-listecho 'KIS-AB12CD-EF34GH-IJ56KL-MN78OP' >> /etc/harbor/keys/revoked.txt# 2. Reload (the list is read at startup)sudo systemctl restart harborThe 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. Client (kvm) usage
Section titled “5. Client (kvm) usage”5.1 Bootstrap install (public, no license)
Section titled “5.1 Bootstrap install (public, no license)”curl -sSL https://get.kisai.dev/install.sh | shexport PATH="$HOME/.kisai/bin:$PATH" # follow the printed instructionThis downloads kvm for your platform into ~/.kisai/bin. (Override the source
with HARBOR_URL=https://your-host:8443.)
5.2 Activate a license
Section titled “5.2 Activate a license”kvm license activate ./acme-license.json # file, or the base64 stringkvm license statusThe license is stored at ~/.kisai/license.json (mode 0600). The server URL
lives in ~/.kisai/config.yaml (harbor.server, default https://get.kisai.dev).
5.3 Install components
Section titled “5.3 Install components”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 artifactkvm install iam.svc # find by name across entitled bucketskvm install all # everything entitledkvm install baas -v 1.2.0 # pin a versionsudo kvm install baas --system # install to /usr/local/bin instead of ~/.kisai/binNo wasted downloads. Before fetching,
kvmcompares the local binary’s SHA-256 against the remote artifact’s. If they match it printsup to dateand transfers nothing — so re-runningkvm install kis-cliwhen everything is current downloads zero bytes. When a binary has changed,kvmdelta-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--forceto 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-clifetches its binaries concurrently rather than one after another.
Browse what’s available:
kvm buckets --entitledkvm bundles --detailedkvm list --remote --full5.4 Update (with anti-rollback)
Section titled “5.4 Update (with anti-rollback)”kvm update # update all installed components to latestkvm update baas # only the baas bucketkvm update --force # re-download everything, even if unchangedkvm list # installed components with their versionsupdate 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:
kvm update --allow-downgradekvm self-update --allow-downgrade5.5 Update kvm itself
Section titled “5.5 Update kvm itself”kvm self-update # no-op if already current (prints "already up to date")kvm self-update --force # re-download even if unchangedself-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.
5.6 Remove
Section titled “5.6 Remove”kvm uninstall iam.svckvm uninstall kis-cli # a whole bundlekvm uninstall --all # remove every component, config, and kvm5.7 How delta updates work
Section titled “5.7 How delta updates work”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:
- Fetches the control blob (
…/control). - 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.
- Fetches only the missing byte ranges via HTTP
Rangerequests. - 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.
Security model
Section titled “Security model”- Signed artifacts. Each artifact is signed with Ed25519 over its SHA-256
digest.
harbor-adminsigns at publish time; the server serves the signature inX-Signatureand the digest inX-Checksum-SHA256. - Embedded trust anchor. The public key is compiled into
kvm(kis flow -t build-kvm, build tagembed_key). Verification therefore holds even if TLS is compromised or a mirror is malicious. - Fail-closed verification.
kvmrefuses 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 strippedX-Signatureheader, 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.
API reference
Section titled “API reference”Public (no auth)
Section titled “Public (no auth)”| Method | Endpoint | Description |
|---|---|---|
| GET | /health | Health check |
| GET | /install.sh | Bootstrap installer script |
| GET | /kvm/{platform} | Download the kvm binary |
| GET | /kvm/{platform}/meta | kvm metadata (version, checksum, signature) |
Protected (license required — Authorization: Bearer <base64-license>)
Section titled “Protected (license required — Authorization: Bearer <base64-license>)”| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/buckets | List 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}/meta | Artifact metadata |
| GET | /v1/artifacts/{bucket}/{name}/{version}/{platform}/control | zsync 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.
Storage layout
Section titled “Storage layout”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.zsyncis generated automatically byharbor-admin upload/sync. Artifacts published before this feature simply have no control blob — clients detect its absence and fall back to a full download. Runharbor-admin artifact reindex(§2.5) to backfill them.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause / fix |
|---|---|
kvm: signature verification required but not possible | The 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 failed | The 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 … skipping | Anti-rollback. The server’s “latest” is older than what you have. Investigate the server; override with --allow-downgrade only if intentional. |
Server: 403 license revoked | The license key or org-id is on --revocation-list. Expected for offboarded customers. |
Server: 403 bucket not entitled | The license lacks that bucket; reissue with the right --entitlements. |
Server: public key path is required | Pass --public-key or set server.public_key in config. |
Server won’t start on :8443 | Port 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).