Harbor
Harbor 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).