Global flags

FlagDefaultMeaning
--socket PATH/var/run/keel-agentd.sockPath to the keel-agentd Unix socket to talk to. Ignored if --control-plane-addr is given.
--control-plane-addr ADDRnoneAddress of a keel-controlplane to route through instead of a local socket. Alone, the control plane schedules the request onto whichever alive node has the most CPU/memory headroom; combined with --node, it routes to that exact node instead.
--node IDnonePins the request to a specific node's keel-agentd instead of letting the control plane schedule it. Requires --control-plane-addr.
--tls-ca-file PATHnoneThe cluster's private CA certificate. Required, together with the other three --tls-*-file flags below, whenever --control-plane-addr is given.
--tls-cert-file PATHnoneThis caller's own certificate, signed by the cluster CA (one per human operator, issued by scripts/gen-certs.sh client <name>; see Certificates below).
--tls-key-file PATHnoneThe private key matching --tls-cert-file.
--tls-crl-file PATHnoneThe cluster's certificate revocation list. Checked on every connection in both directions; a revoked certificate, either the caller's own or the control plane's, fails the TLS handshake.

Giving --node without --control-plane-addr is a usage error. Giving --control-plane-addr alone is valid: it schedules automatically. Giving neither preserves keelctl's original behavior exactly: talk directly to --socket (or its default).

Giving --control-plane-addr without all four --tls-*-file flags is also a usage error, and fails locally before keelctl ever reaches the network. Every control-plane-routed connection is mutual TLS: keelctl proves its own identity with --tls-cert-file/--tls-key-file, and verifies the control plane's certificate against --tls-ca-file and --tls-crl-file, the same way the control plane verifies keelctl's.

Certificates

Control-plane-routed mode never involves keelctl generating or handling key material on its own: an operator generates a private CA once, then issues one client certificate per human operator and one dual-use certificate per node or control-plane instance, using scripts/gen-certs.sh.

./scripts/gen-certs.sh init                # generate the CA once
./scripts/gen-certs.sh client alice        # issue alice's own operator certificate
./scripts/gen-certs.sh revoke alice        # eject a compromised identity, refreshes crl.pem
./scripts/gen-certs.sh crl                 # refresh crl.pem with no status change

Reissuing an existing name (client alice again) rotates it: the new certificate is issued first, and only once that succeeds is the previous certificate revoked and crl.pem regenerated, so a failed reissue never strands an identity with zero valid certificates. keel-controlplane and keel-agentd both reload their certificate, key, CA, and CRL files from disk on a background timer (every 30 seconds), so a rotated certificate or a refreshed CRL takes effect without restarting either daemon; a keelctl invocation, being a single short-lived process, always reads the current files fresh on every run.

keelctl apply -f FILE

Reads a spec YAML file, validates it locally, and sniffs kind to decide where it goes: a kind: Jail sends PUT /jails/<name>; a kind: Service sends PUT /services/<name>; a kind: Ingress sends PUT /ingress/<name> (the name comes from the spec's own metadata.name in every case). Routed through a control plane with no --node, a kind: Jail's PUT /jails/<name> is scheduled automatically onto the least-loaded alive node; with --node, it's PUT /nodes/<node>/jails/<name> instead, landing on exactly that node. A kind: Service always goes through the control plane's own scheduler and same-service spreading; --node does not apply to it. A kind: Ingress is never routed through a control plane at all: it always applies directly to --socket's node, regardless of --control-plane-addr/--node, since an Ingress and its backend Service must already be co-located on one node (see the HTTP API reference).

keelctl apply -f jail.yaml
keelctl --control-plane-addr 10.0.0.2:7620 apply -f jail.yaml
keelctl --control-plane-addr 10.0.0.2:7620 --node node-4 apply -f jail.yaml
keelctl --control-plane-addr 10.0.0.2:7620 apply -f service.yaml
keelctl apply -f ingress.yaml

keelctl get [name]

With no argument, lists every jail keel-agentd (or the routed-to node) is tracking (GET /jails, or GET /nodes/<node>/jails when routed by name); this never lists services or ingresses, see the notes below. With a name, tries GET /jails/<name> first, then falls back to GET /services/<name> on a 404 (a service's currently-healthy replicas), then to GET /ingress/<name> on a further 404, since all three share one flat namespace and a 404 on one path is a cheap, unambiguous signal to try the next. Through a control plane with no --node, the jail-path lookup resolves against whichever node the jail was placed on; with --node, GET /nodes/<node>/jails/<name>. The Ingress fallback leg always targets the local --socket, the same reasoning apply above uses.

keelctl get
keelctl get web-1
keelctl --control-plane-addr 10.0.0.2:7620 get web-1
keelctl --control-plane-addr 10.0.0.2:7620 --node node-4 get web-1
keelctl --control-plane-addr 10.0.0.2:7620 get web       # falls back to the service, if web-1 above doesn't exist
keelctl get blog                                         # falls back further to the Ingress, if neither above exists

No bare-collection verb for services. keel-controlplane's GET /services (every known service, including each one's virtual IP and port, see the HTTP API reference) has no corresponding keelctl command: get either lists jails (no argument) or resolves a single name (jail, falling back to service, falling back to ingress). Reaching a service's VIP today means a direct HTTP call against the control plane.

keelctl delete NAME

Tears down the named jail, its ZFS dataset, its network attachment, and its resource limits (DELETE /jails/<name>, or DELETE /nodes/<node>/jails/<name> when routed by name), falling back to DELETE /services/<name> then DELETE /ingress/<name> on successive 404s the same way get does, tearing down every one of a service's replicas (and, once its next heartbeat lands on every node, its virtual IP alias), or removing an Ingress's server block from nginx on the node's next reconcile pass. Through a control plane with no --node, deleting a jail also clears its recorded placement.

keelctl delete web-1
keelctl --control-plane-addr 10.0.0.2:7620 delete web-1
keelctl --control-plane-addr 10.0.0.2:7620 --node node-4 delete web-1
keelctl --control-plane-addr 10.0.0.2:7620 delete web    # falls back to the service, if web-1 above doesn't exist
keelctl delete blog                                      # falls back further to the Ingress, if neither above exists

keelctl delete-volume NAME

Destroys a named volume's ZFS dataset for good (DELETE /volumes/<name>, or DELETE /nodes/<node>/volumes/<name> when routed with --node). A volume is never scheduled on its own, so there's no equivalent of automatic node selection here the way apply has: routed mode always needs an explicit --node naming the node the volume's dataset actually lives on. Fails with a clean error (rather than force-unmounting anything) if a jail still has the volume mounted; delete that jail first.

keelctl delete-volume web-data
keelctl --control-plane-addr 10.0.0.2:7620 --node node-4 delete-volume web-data

There is no keelctl get-volume or bare-collection verb for volumes; see the HTTP API reference for the full GET/DELETE /volumes/<name> surface, reachable directly over HTTP.

keelctl force-repin NAME

Promotes a stateful replica's standby to primary (POST /replicas/<name>/force-repin), for the case where the current primary's node is genuinely down and its replicated data needs to keep serving from where it was continuously replicated to. Refuses if the current primary still resolves as alive, if the replica isn't stateful (no recorded standby), or if the standby hasn't finished a first full replication yet. See the Architecture page for how the standby is chosen and kept in sync in the first place.

keelctl --control-plane-addr 10.0.0.2:7620 force-repin db-0

keelctl cordon / uncordon NODE

Marks a node unschedulable (cordon) or schedulable again (uncordon), with zero disruption to whatever is already running there. Neither verb touches existing placements; a cordoned node keeps serving everything it already hosts, it just stops being picked for anything new.

keelctl --control-plane-addr 10.0.0.2:7620 cordon node-3
keelctl --control-plane-addr 10.0.0.2:7620 uncordon node-3

keelctl drain NODE

Actively empties a node of everything it hosts, so it becomes safe to take down for maintenance. Refuses upfront against a node that's already Dead (use force-repin instead) or one hosting a live Ingress (no automated cross-node Ingress migration exists yet). Migrates each placement off by kind: stateless service replicas are removed and left to the next heartbeat's self-healing, stateful replicas are force-repinned to a fresh standby, and plain jails are re-placed elsewhere before the old copy is deleted. Draining doesn't cordon the node as a side effect; run cordon first if the node should also stay unschedulable afterward.

keelctl --control-plane-addr 10.0.0.2:7620 cordon node-3
keelctl --control-plane-addr 10.0.0.2:7620 drain node-3

keelctl backup create / backup list

backup create triggers a point-in-time, cluster-wide backup: the control plane's own state, every node's agent state, and every node's volume data (POST /backup), printing a manifest of which nodes and volumes succeeded, failed, or were skipped. backup list reads back every saved manifest (GET /backup). Both are always sent to the control plane directly, never --node-scoped, since a cluster backup is never a per-node operation from the operator's perspective. See the Architecture page for what actually gets captured and where.

keelctl --control-plane-addr 10.0.0.2:7620 backup create
keelctl --control-plane-addr 10.0.0.2:7620 backup list

keelctl restore ID --yes

The destructive mirror of backup create: tears down every jail on every reachable node, wipes and replaces state directories, and restores volume data from the named backup (POST /restore/<id>). Refuses to run without the explicit --yes flag, checked locally before any network call, and the control plane 404s on an unknown id before touching any node's live state. Prints a reminder on success that both the control plane and every restored node's keel-agentd need an operator-triggered restart afterward, since neither hot-reloads restored state.

keelctl --control-plane-addr 10.0.0.2:7620 backup list          # find the id to restore
keelctl --control-plane-addr 10.0.0.2:7620 restore 2026-08-07T09-56-20Z --yes

Errors

On any non-2xx response, keelctl prints the server's error message to stderr and exits non-zero. See the HTTP API reference for the specific error cases each endpoint can return.