π Docs site: https://kanadehq.github.io/kanade/
ε₯ β orchestrate. A self-hosted Rust pub/sub backbone for managing thousands of Windows endpoints without Active Directory. NATS / JetStream carries inventory polling, fleet-wide rollouts, and ad-hoc emergency commands on a single channel.
Status: 0.1.0 β Sprint 4 shipped. Agent + backend (axum + SQLite
projector + JetStream KV watcher + cron scheduler) + admin CLI + an
embedded SPA dashboard + JWT-gated /api/* + agent self-update via the
JetStream Object Store. Full design lives in
docs/SPEC.md (Japanese, ~1150 lines covering Part 1
overview and Part 2 detailed design).
Why
The off-the-shelf endpoint managers (Intune, Tanium, Workspace ONE, β¦) either require Active Directory, lock you into a vendor cloud, or both. For shops that want AD-independent, on-prem, scriptable fleet control the answer has historically been "build something on top of a message broker" β which everyone reinvents from scratch.
kanade aims to be the reusable shape of that build:
- NATS + JetStream as the only moving part. Agents speak to the broker over outbound TLS; the broker fans out commands, fans in inventory and results. No AD, no client-pull-from-server, no opening inbound ports on user PCs.
- Declarative job manifests in Git. Review, history, rollback all
come for free; the YAML schema (
jobs/*.yaml) is the same input whether youkanade execad-hoc or wire it onto a cronkanade schedule. - Three layers of stop-the-bleed. Stream max-msgs-per-subject
replaces stale rollouts in the broker; consumer-side version checks
guard execution;
kanade kill <job_id>terminates running children. The emergency-stop path is wired from MVP, not bolted on later (see SPEC.md Β§2.6). - Phased build-out. One server is enough for a few hundred endpoints; the same code scales to a 3-node NATS cluster + replicated backend + Postgres for several thousand.
Crates
| crate | kind | role |
|---|---|---|
kanade-shared |
lib | wire types (Command / ExecResult / Heartbeat / HwInventory), NATS subject + KV helpers, YAML manifest schema, teravars-backed config loader |
kanade-agent |
bin | Windows-side resident daemon: subscribes to commands.*, runs child processes, publishes results + heartbeats + WMI inventory; watches the layered agent_config + agent_groups KV buckets and reacts live to cadence / membership / target_version changes |
kanade-backend |
bin | axum HTTP server: /health, /api/{agents,results,audit,deploy,schedules,config,β¦}, embedded SPA at /. Auto-bootstraps every required JetStream resource at startup, runs durable projectors (INVENTORY/RESULTS/AUDIT β SQLite) and a tokio-cron-scheduler driven by the schedules KV |
kanade |
bin | operator-side admin CLI (kubectl-style single entry point); subcommands talk to NATS directly for run/kill/jetstream and to the backend over HTTP for agent/ping/revoke/config/deploy/schedule |
Install
You'll need:
- Rust 1.85+ (the workspace pins
edition = "2024") - A NATS server (Go binary, ~15 MB)
# 1. NATS server
scoop install nats-server # or: winget install nats-io.nats-server
# 2. The three kanade binaries β straight from crates.io.
cargo install kanade kanade-agent kanade-backend
kanade, kanade-agent, and kanade-backend are now on your PATH
(under ~/.cargo/bin/).
You'll also want the sample configs (configs/agent.toml / configs/backend.toml) and
the example manifests (jobs/*.yaml). The fastest way is a shallow
clone of this repo:
git clone --depth=1 https://github.com/kanadehq/kanade.git
cd kanade
(or curl the individual files from
https://raw.githubusercontent.com/kanadehq/kanade/main/... into your
own working dir if you'd rather not clone).
Build it yourself from source. Skip the
cargo installstep,git clonethe full repo, and runcargo install --path crates/kanade --path crates/kanade-agent --path crates/kanade-backend(one--pathat a time, or repeat the command three times). That path matters if you're hacking on the crates.
Quick start (4 terminals, ~2 minutes)
Run each step in its own PowerShell window so the daemons stay up. All
of them assume cd into the repo root (which holds configs/agent.toml
/ configs/backend.toml / jobs/).
1 β start NATS
nats-server -js -p 4222
2 β start the backend
$env:KANADE_AUTH_DISABLE = "1" # JWT off for development
kanade-backend
The first time it starts against a broker it creates every stream, KV
bucket and Object Store itself, so a fresh NATS server + kanade-backend
is enough to get a working fleet (kanade jetstream status shows what
exists). If a stream has drifted or is corrupted so the backend cannot
start, stop it and use the nats CLI with an administrative credential
(not kanade) via the operator scripts, then start it again:
./scripts/ops/jetstream-delete.ps1 -Kind stream -Name RESULTS -Server nats://127.0.0.1:4222 -Creds ./admin.creds
./scripts/ops/jetstream-reset.ps1 -Server nats://127.0.0.1:4222 -Creds ./admin.creds # dry run
./scripts/ops/jetstream-reset.ps1 -Server nats://127.0.0.1:4222 -Creds ./admin.creds -Yes # wipe all kanade resources
The backend serves the dashboard at http://127.0.0.1:8080 and the JSON API at
/api/*. SQLite is created at ./backend.db. Both projectors and the
cron scheduler start in the background.
3 β start the agent
kanade-agent
Loads ./configs/agent.toml, picks $env:COMPUTERNAME as pc_id,
subscribes to commands.all + commands.pc.{pc_id}, then spawns the
config_supervisor (watches agent_config + agent_groups KV) plus
the heartbeat / inventory / self-update / groups-manager loops. Group
membership and cadence settings are read from the KV buckets β see
kanade group (via the backend API) and kanade config to drive them.
4 β drive it
# Round-trip a script via NATS, request/reply.
kanade run $env:COMPUTERNAME -- 'echo hello from kanade'
# Or via the backend's YAML deploy path (writes a row to deployments,
# emits an audit event, broadcasts the Command).
kanade exec jobs/echo-test.yaml
# Liveness probe (the backend asks the agent; needs KANADE_AUTH_TOKEN).
kanade ping $env:COMPUTERNAME
# Inspect via curlβ¦
curl http://127.0.0.1:8080/api/agents
curl http://127.0.0.1:8080/api/results
curl http://127.0.0.1:8080/api/audit
# β¦or open the dashboard.
start http://127.0.0.1:8080
CLI cheat sheet
kanade run <pc_id> -- <script> # request/reply via NATS
kanade ping <pc_id> [--wait N] # ask the agent for a fresh heartbeat (via backend API)
kanade kill <job_id> # publish kill.{job_id}
kanade revoke <cmd_id> # script_status = REVOKED (via backend API)
kanade unrevoke <cmd_id> # β ACTIVE (via backend API)
kanade jetstream status # health snapshot (via backend API)
kanade job create <path...> # upsert into the jobs catalog; accepts files / dirs / globs (configs/jobs/*.yaml)
kanade job export <id> [--out-dir <dir>] # dump registered YAML to stdout (or <dir>/<id>.yaml)
kanade job export --all --out-dir <dir> # dump every registered job to <dir>/<id>.yaml
kanade job list # every registered job
kanade job delete <id> # refuses if any schedule references it
kanade exec <job-id> # fire a registered job ad-hoc (POST /api/exec/<id>)
kanade app publish <name> <file> [--version V] # upload an installer to the app-packages store (via backend API; operator role)
kanade app list | delete <name> <version> # (via backend API)
kanade script publish <name> <version> <file> # upload a manifest script body, 4 MB max (via backend API; operator role)
kanade script list | delete <name> <version> # (via backend API)
kanade schedule create <path...> # cron yaml { id, cron, job_id, enabled }; accepts files / dirs / globs
kanade schedule export <id> [--out-dir <dir>] # dump registered YAML to stdout (or <dir>/<id>.yaml)
kanade schedule export --all --out-dir <dir> # dump every registered schedule
kanade schedule list
kanade schedule delete <id>
# `kanade agent β¦` goes through the backend HTTP API (KANADE_AUTH_TOKEN; operator role to publish
# or roll out) β no NATS connection or broker token needed. `publish` is capped by the backend at
# 64 MB for the whole upload.
kanade agent publish <binary> [--version <v>] # upload binary to Object Store (no KV touch)
kanade agent rollout <v> --global [--jitter <d>] # fleet-wide
kanade agent rollout <v> --group <name> [--jitter <d>] # canary / wave
kanade agent rollout <v> --pc <pc_id> [--jitter <d>] # single-host pin
kanade agent current # read agent_config.global.target_version (global scope only)
kanade agent logs <pc_id> [--tail <n>] # tail an online agent's log file (via the backend)
# `kanade group β¦` goes through the backend HTTP API (KANADE_AUTH_TOKEN; operator role to change
# membership) β no NATS connection or broker token needed.
kanade group list # fleet-wide: every known group + member count + config flag
kanade group list --pc <pc_id> # one PC's memberships
kanade group members <name> # PCs in this group
kanade group add <pc_id> <name> # add membership (idempotent)
kanade group rm <pc_id> <name> # drop membership
kanade group set <pc_id> <name> ... # replace whole list
# `kanade config β¦` goes through the backend HTTP API (KANADE_AUTH_TOKEN; operator role to change
# anything) β no NATS connection or broker token needed. Automation that calls it authenticates
# with an auth token like the other HTTP subcommands.
kanade config get [--group <name>|--pc <pc_id>] # ConfigScope at this scope (default: global)
kanade config set <field>=<value> [...] # set one field (target_version / inventory_* / heartbeat_*)
kanade config unset <field> [...] # clear one field
kanade config clear [--group <name>|--pc <pc_id>] # delete the whole scope row
kanade config effective <pc_id> # resolved view for a PC (built-in -> global -> groups -> pc)
# `kanade meta β¦` (per-PC operator key/value metadata) goes through the backend HTTP API
# (KANADE_AUTH_TOKEN; operator role to change anything) β no NATS connection or broker token
# needed, and the backend must be up. A directory-sync job that calls `kanade meta set` from the
# backend host authenticates with an auth token too. `set` / `rm` touch only their own key (the
# backend does the compare-and-swap), so they never drop keys written concurrently.
kanade meta get <pc_id> # all attributes
kanade meta set <pc_id> <key> <value> # upsert one key (empty value keeps the key, blank)
kanade meta rm <pc_id> <key> # drop one key (idempotent)
kanade meta clear <pc_id> # drop every key
kanade <subcommand> --help for argument details.
Authoring jobs
YAML manifests in jobs/*.yaml (see spec Β§2.4.1).
Sample manifests in the repo cover:
jobs/echo-test.yamlβ minimal ad-hoc commandjobs/wave-test.yamlβrollout.wavesrollout (canary β wave1 with delay)jobs/schedule-test.yamlβ cron-driven echo every 10 s
A wave manifest sketch:
id: cleanup-disk-temp
version: 1.0.1
target:
pcs:
execute:
shell: powershell
script: |
$temp = [System.IO.Path]::GetTempPath()
Remove-Item "$temp\*" -Recurse -Force -ErrorAction SilentlyContinue
timeout: 600s
jitter: 5m
rollout:
strategy: wave
waves:
-
-
Config files
Both use teravars templating β {{ system.host }}, {{ env(name="X", default="Y") }}, {% if is_windows() %}β¦{% endif %} are all available.
agent.toml (intentionally minimal β fleet policy lives in the
agent_config + agent_groups KV buckets, edited via
kanade config / kanade agent groups):
[]
= '{{ system.host }}'
= 'nats://127.0.0.1:4222'
[]
= 'logs/agent.log'
= 'info'
Older agent.toml files that still carry [agent] groups = [β¦] or an
[inventory] section keep loading β both fields are parsed via
#[serde(default)] β but the values are logged-and-ignored at
startup. Removal is scheduled for v0.4.0.
backend.toml:
[]
= '0.0.0.0:8080'
[]
= 'nats://127.0.0.1:4222'
[]
= './backend.db'
[]
= 'logs/backend.log'
= 'info'
Authentication & RBAC
/api/* is protected by a single middleware (crates/kanade-backend/src/auth.rs).
A request is admitted by the first of these that matches:
| Mode | Selector | Use for |
|---|---|---|
| open | KANADE_AUTH_DISABLE=1 |
local dev, cargo run (synthesises an admin) |
| service token | StaticToken registry value or $KANADE_AUTH_STATIC_TOKEN |
CI / non-interactive automation β admin-equivalent, no account row |
| account JWT | username + password β POST /api/auth/login mints an HS256 token |
the normal path for humans; carries the caller's role |
A non-matching bearer falls through from the service token to JWT validation, so a service token and per-user JWTs coexist. With none of the three set the backend falls back to a hard-coded dev secret and logs a loud warning β fine for one-shot debugging, never for production.
Roles
Accounts live in the SQLite users table (argon2id password hashes). Each
account has one hierarchical role β admin β operator β viewer:
| Role | Can |
|---|---|
viewer |
read-only β every GET /api/* (dashboards, inventory, logs, audit) |
operator |
viewer + fleet mutations (exec / kill / schedules / jobs / config / releases / uploads) |
admin |
operator + account management (create / role / disable / delete) |
Enforcement is server-side (route_layer guards reject 403); the SPA also
hides controls above the caller's role. Role and disabled are re-read from
the DB on every request, so disabling an account or changing its role takes
effect immediately β existing tokens don't stay valid until expiry.
Secrets
JwtSecret signs and verifies the minted tokens; it's the fleet-wide skeleton
key, so it lives only on the backend host. Each secret resolves
registry-first, env-second:
StaticToken: HKLM\SOFTWARE\kanade\backend\StaticToken β $KANADE_AUTH_STATIC_TOKEN
JwtSecret: HKLM\SOFTWARE\kanade\backend\JwtSecret β $KANADE_JWT_SECRET
BootstrapAdminPassword:HKLM\SOFTWARE\kanade\backend\BootstrapAdminPassword β $KANADE_BOOTSTRAP_ADMIN_PASSWORD
Provision the registry values with deploy-backend.ps1 so the script can
strip non-admin ACEs from the key (SYSTEM + Administrators read only). The
env vars stay for cargo run / cargo make dev / non-Windows hosts.
KANADE_AUTH_DISABLE stays env-only β it's a presence flag, not a secret.
Note: the registry values are stored as plaintext
REG_SZprotected by a SYSTEM + Administrators ACL. That defends against non-admin users but not against offline disk / hive-backup theft; DPAPI / external-vault hardening is a tracked follow-up.
Bootstrapping the first admin
On startup, if the users table is empty the backend seeds a single admin
from BootstrapAdminPassword (registry) / $KANADE_BOOTSTRAP_ADMIN_PASSWORD
(username defaults to admin, override with $KANADE_BOOTSTRAP_ADMIN_USER).
The seeded account is flagged must-change-password. If no bootstrap password is
configured, no admin is seeded (a warning is logged) β set one and restart, or
fall back to the static service token.
Clients
Clients send Authorization: Bearer <token> on every /api/* request:
- SPA: sign in with username + password on the login page; the token is
kept in
localStorage. Role is shown in the sidebar; admins get an Accounts page. A 401 auto-clears the token and re-prompts. - CLI:
kanade loginexchanges credentials for a token; export it as$env:KANADE_AUTH_TOKENfor subsequent commands.kanade account β¦(admin) manages users.
# Backend side β production (registry, hardened ACL)
.\deploy-backend.ps1 -JwtSecret 'sign-key-2026' -BootstrapAdminPassword 'change-me-now'
# Operator side (CLI) β log in, then run fleet commands
$env:KANADE_AUTH_TOKEN = (kanade login --username admin --token-only)
kanade account create alice --role operator # admin only
kanade exec jobs\echo-test.yaml # operator+
# CI / automation β admin-equivalent service token, no login round-trip
$env:KANADE_AUTH_TOKEN = "<your-fleet-token>" # matches backend StaticToken
NATS authentication
Separate from the backend HTTP layer above. By default nats-server -js
listens on :4222 without auth β anyone on the LAN who can reach the broker
can publish commands.pc.<host> and execute scripts on every agent. Lock
it down for production with token auth:
-
Start nats-server with the bundled config:
nats-server -c configs/nats-server.confThe shipped
configs/nats-server.confenables JetStream + anauthorization.tokenblock. Pick your own secret. For production, run as a Windows service viadeploy-nats.ps1(the script applies a SYSTEM + Administrators-only ACL on the installed config so the token isn't readable by other users).Generate your own token β never ship the sample. The value in
configs/nats-server.confis a placeholder committed to a public repository, so a fleet deployed with it has a credential anyone can read. Mint one per fleet, e.g.[Convert]::ToBase64String((1..32|%{Get-Random -Max 256})). -
Provision the token on every kanade host. The shared
kanade_shared::nats_client::connect()helper takes the caller's role (agent / backend / cli) and resolves the token in this order:(1)
HKLM\SOFTWARE\kanade\<role>\NatsToken(REG_SZ) β production.deploy-agent.ps1/deploy-backend.ps1accept-NatsTokenand write the value with a hardened ACL (SYSTEM + Administrators only). Low-privilege users on the host cannot read it back, which a Machine-scope environment variable cannot prevent.(2)
HKLM\SOFTWARE\kanade\agent\NatsTokenβ the shared fallback. Before roles existed every binary read this one path, so it stays as a fallback and an unmigrated fleet keeps working unchanged. Splitting the credentials is what lets the broker tell the roles apart, which is a prerequisite forauthorization { users: [...] }subject permissions β see #1155 for why one fleet-wide token is worth moving off.# On agent + backend hosts .\deploy-agent.ps1 -NatsToken '<your-fleet-token>' .\deploy-backend.ps1 -NatsToken '<your-fleet-token>'(3)
$KANADE_NATS_TOKENenvironment variable β dev / fallback. Used only when neither registry value is present. Service binaries run as LocalSystem and never see user-session env vars, so this branch fires forcargo run, the operator CLI, andcargo make dev:$env:KANADE_NATS_TOKEN = '<your-fleet-token>' kanade run <pc_id> -- hostname(3) No token β unauthenticated connect. Works against a broker started without
authorization { ... }β fine for local dev.
nats_url in agent.toml / backend.toml stays plain. The secret
never lands in config files or process listings.
Seeing which credential each host actually used
Provisioning a credential and a host presenting it are different facts,
and only the second one is worth acting on. The backend polls the broker's
monitoring endpoint (/connz) every 60 s and records, per host, the NATS
user its live connection authenticated as β reported by NATS, not by the
agent, because a host kitted from a stale image is exactly the one whose
own account of itself cannot be trusted (#1270). It surfaces on
GET /api/agents as nats_user, with nats_user_since marking when that
last changed:
| value | meaning |
|---|---|
| (absent) | never correlated β no live connection seen, or the agent predates #1270 and announces no pc_id. Unknown, not "old credential". |
shared-token |
the single fleet-wide token. The migration queue. |
no-auth |
the broker authenticated nobody (no authorization block). |
unknown |
connected, credential unnameable without printing a secret. |
| anything else | the NATS username, verbatim. |
# How many hosts are still on the shared token?
(irm http://<backend>/api/agents -Headers $h) |
Group-Object nats_user | Select-Object Count, Name
Under today's token auth every host that correlates lands in the one
shared-token bucket β hosts whose agent announces no pc_id stay absent,
and a connection the backend cannot vouch for reads unknown. That is the
endpoint's answer rather than a shortcut: nats-server reports which
credential a connection authenticated with, and for token auth it reports
only that a token was used ([REDACTED], measured on 2.14.3). A users
split is what makes the answer per-role β and the value is never stored raw
regardless, because whether the broker hides a credential is the broker
build's choice, not this code's.
Treat the monitoring port as sensitive: it enumerates every connection, its
IP and its subscriptions, and NATS provides no authentication for it. Both
shipped broker configs bind it to loopback (http: "127.0.0.1:8222"), which
is right for the usual deploy where the backend runs on the broker host β
note that the bare http_port: 8222 form listens on all interfaces. If
you split backend and broker across hosts, widen the bind to a management
interface and set monitor_url to match.
Set [nats] monitor_url in backend.toml if monitoring does not live at
http://<nats-host>:8222. If it is unreachable or switched off, the
projection simply stops updating: values already recorded are kept (they
remain the best answer about each host), and a host that was never
correlated stays empty until the first successful poll.
For multi-tenant / per-agent identity (NKeys, NATS JWT, mTLS), see spec Β§2.7.1. Stick with the shared token while operating β€ ~1000 hosts.
Dev workflow
cargo make check # fmt-check + clippy + test + lock-check (same as CI)
cargo make fmt # apply formatting
cargo make on-add # renri post_create hook (apm install + vcs fetch)
The workspace pins [profile.dev] debug = "line-tables-only" because
Windows MSVC link.exe hits LNK1318 (PDB record limit) once axum +
sqlx + reqwest + tokio-cron-scheduler + jsonwebtoken all sit in one
workspace; line-tables-only keeps backtraces useful without exploding
the PDB.
Integration tests (kanade-agent)
crates/kanade-agent/tests/offline_boot.rs exercises the offline-
tolerant boot path from #137 end-to-end by spawning a real
nats-server and the agent binary. The tests are #[ignore]-gated
so cargo test (and cargo make check) stays fast for local dev
without needing nats-server on every machine. The dedicated
Integration workflow installs nats-server per runner (Linux /
macOS / Windows) and opts in via --ignored.
To run them locally, install nats-server first:
| Platform | Command |
|---|---|
| Windows | scoop install nats-server |
| macOS | brew install nats-server |
| Linux | GitHub release, or your distro's package manager |
Then:
cargo test -p kanade-agent --test offline_boot -- --ignored --nocapture
Total runtime is ~30 s. Each test spawns its own broker on a free
port and a fresh agent process with a temp agent.toml, asserts
either "agent stays alive without broker" or "agent's heartbeat
arrives within timeout" depending on the case, and tears everything
down on Drop. If nats-server isn't on PATH, each test logs a
clear "skipping" message instead of panicking.
Production install layout
cargo install drops the binaries under ~/.cargo/bin/ (user-local).
For a real deployment, copy them into the spec Β§2.11 layout and register
a service so they survive reboots.
Path layout
Windows Linux
C:\Program Files\Kanade\ /usr/local/bin/
βββ kanade-agent.exe βββ kanade-agent
βββ kanade-backend.exe βββ kanade-backend
βββ kanade.exe βββ kanade
βββ nats-server.exe βββ nats-server
C:\ProgramData\Kanade\config\ /etc/kanade/
βββ agent.toml βββ agent.toml
βββ backend.toml βββ backend.toml
βββ nats-server.conf (hardened ACL) βββ nats-server.conf
C:\ProgramData\Kanade\data\ /var/lib/kanade/
βββ state.db (agent) βββ state.db
βββ outbox\ (agent) βββ outbox/
βββ staging\ (self-update) βββ staging/
βββ backend.db (backend) βββ backend.db
βββ certs\ βββ certs/
βββ nats\ (JetStream data) βββ nats/
C:\ProgramData\Kanade\logs\ /var/log/kanade/
βββ agent.log βββ agent.log
βββ backend.log βββ backend.log
βββ nats-server.log βββ nats-server.log
Config discovery
Every binary looks up its config file in this exact order (no cwd fallback β too easy to load the wrong file by accident):
--config <path>CLI flag (always honored, even if the file doesn't exist β that's the caller's choice).- Environment variable:
KANADE_AGENT_CONFIGforkanade-agent,KANADE_BACKEND_CONFIGforkanade-backend. Non-empty value wins. <config_dir>/<basename>:- Windows:
%ProgramData%\Kanade\config\agent.toml - Linux:
/etc/kanade/agent.toml
- Windows:
If none of the three is reachable, the binary exits with a message listing every option an operator can use to fix it.
Install scripts (Windows, recommended)
PowerShell scripts under
scripts/
handle the whole "drop a folder onto the target, run as Admin" path
β no Rust toolchain, no bun, no git required on the deploy host:
# 1. On any Windows box (no dev tooling needed): pull pre-built
# binaries straight from GitHub Releases via Invoke-WebRequest
# and assemble one stage folder per role under .\dist\.
PS> .\scripts\build-release.ps1
# β dist\agent\, dist\backend\, dist\nats\
# each contains: <role>.exe, <role>.{toml,conf}, deploy-<role>.ps1
#
# Variants:
# -Roles agent,backend # skip nats
# -NatsVersion 2.11.10 # pin a specific NATS broker tag
# -FromSource # compile from this checkout (cargo + bun required)
# -FromCrates # install from crates.io (cargo required)
# -Zip # also produce dist\<role>.zip
# 2. Copy each stage folder onto the target host (xcopy, robocopy,
# scp, USB stick β whatever fits your environment).
# 3. On the target host, run the matching script as Administrator:
# (<your-fleet-token> is one you generated β see "NATS authentication";
# the value in configs/nats-server.conf is a public placeholder.)
PS> .\deploy-nats.ps1 -NatsToken '<your-fleet-token>' # broker host (run once)
PS> .\deploy-agent.ps1 -NatsToken '<your-fleet-token>' # every endpoint
PS> .\deploy-backend.ps1 -NatsToken '<your-fleet-token>' `
-StaticToken '<api-token>' # admin box
Re-running the script upgrades the binary in place and preserves
the edited config. Pass -ForceConfig to overwrite the installed
config from the source folder, or -NoStart to skip the
post-install service start.
Windows Service registration (sc.exe)
If you'd rather not use the deploy scripts (or want to understand exactly what they do), here are the equivalent manual commands:
# Stage the binaries
New-Item -ItemType Directory -Force 'C:\Program Files\Kanade'
Copy-Item "$env:USERPROFILE\.cargo\bin\kanade-agent.exe" 'C:\Program Files\Kanade\'
Copy-Item "$env:USERPROFILE\.cargo\bin\kanade-backend.exe" 'C:\Program Files\Kanade\'
# Stage the config (review + edit first)
New-Item -ItemType Directory -Force 'C:\ProgramData\Kanade\config'
Copy-Item .\configs\agent.toml 'C:\ProgramData\Kanade\config\'
Copy-Item .\configs\backend.toml 'C:\ProgramData\Kanade\config\'
# Register the agent as a service running under LocalSystem.
sc.exe create KanadeAgent `
binPath= '"C:\Program Files\Kanade\kanade-agent.exe"' `
start= auto `
obj= LocalSystem `
DisplayName= "Kanade Endpoint Agent"
sc.exe failure KanadeAgent reset= 86400 actions= restart/60000/restart/60000/restart/60000
# Register the backend the same way.
sc.exe create KanadeBackend `
binPath= '"C:\Program Files\Kanade\kanade-backend.exe"' `
start= auto `
obj= LocalSystem `
DisplayName= "Kanade Backend"
sc.exe start KanadeAgent
sc.exe start KanadeBackend
Linux systemd units
# /etc/systemd/system/kanade-backend.service
[Unit]
Description=Kanade Backend
After=network.target nats.service
[Service]
ExecStart=/usr/local/bin/kanade-backend
Restart=always
User=kanade
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
The agent unit is symmetric (kanade-agent.service, ExecStart=/usr/local/bin/kanade-agent).
Scaffolded with kata
The skeleton (AGENTS.md / Makefile.toml / clippy.toml /
rustfmt.toml / .github/workflows/* / etc.) was applied via
github.com/yukimemi/pj-presets:rust-cli
through kata init. The Cargo workspace layout under crates/ is
hand-written because the preset is single-crate by default; a
pj-rust-workspace layer is on the future TODO once the multi-crate
patterns stabilise.
License
MIT β see LICENSE.
The shipped binaries statically link their Rust dependencies, and
kanade-backend embeds the compiled SPA, so those third-party licences
travel with the artifacts:
THIRD-PARTY-NOTICES.md
(generated by cargo make notices, kept current by the Licenses
workflow). No dependency is under the GPL, the AGPL, or an LGPL-only
licence; a handful are under the file-scoped MPL-2.0, which the notices
file names explicitly.