keelctl CLI reference
keelctl talks to keel-agentd's local HTTP API over its Unix socket, or routes through keel-controlplane to a specific node.
Global flags
| Flag | Default | Meaning |
|---|---|---|
--socket PATH | /var/run/keel-agentd.sock | Path to the keel-agentd Unix socket to talk to. Ignored if --control-plane-addr is given. |
--control-plane-addr ADDR | none | Address 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 ID | none | Pins the request to a specific node's keel-agentd instead of letting the control plane schedule it. Requires --control-plane-addr. |
--tls-ca-file PATH | none | The cluster's private CA certificate. Required, together with the other three --tls-*-file flags below, whenever --control-plane-addr is given. |
--tls-cert-file PATH | none | This 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 PATH | none | The private key matching --tls-cert-file. |
--tls-crl-file PATH | none | The 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.
Keel