The first pass left routes that hand the body straight to a service
(create_zone(data), MonitorService.create(data), ...). Those services
read it as a dict, so a body of null, [] or a bare string still
crashed them into a 500. Agent long-poll, external webhook receivers
and the routes that already type-check their body are left alone.
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.
Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
Introduces a new `.coderabbit.yaml` with ServerKit-specific review guardrails, and expands env/security documentation for trusted proxy headers and per-IP login brute-force throttling. It also adds inline backend notes clarifying `request.remote_addr` behavior under ProxyFix for auth, cron, and pairing paths, and updates the frontend plugin manifest to remove the Tramo and K8s plugin entries.
Pairing /claim creates a fleet server and /lookup reveals a pending agent's
details; both were @jwt_required() only, so any authenticated user (incl. a
read-only viewer) could use them. Gate both with @developer_required to match
POST /servers. Test: viewer 403, developer clears the gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
During short-code pairing the agent submitted its Ed25519 public key at
enroll time, but the panel never checked that the poller actually held the
matching private key -- identity rested entirely on the bearer
enrollment_secret. Anyone who captured that secret (e.g. from a log or proxy)
could claim the device's credentials.
The agent now signs the enrollment_id with its pairing keypair and sends the
base64 signature as X-Enrollment-Signature on each poll. pairing_service.poll
verifies it against the enrolled hex pubkey (cryptography Ed25519) and rejects
a present-but-invalid or wrong-key signature with InvalidEnrollmentError,
before touching any state. So a stolen enrollment_secret alone no longer
suffices -- the attacker also needs the private key.
Verification is best-effort for backward compatibility: a poll with no
signature still works (older agents), and becomes mandatory once signing
agents have rolled out (tracked in SECURITY.md).
Agent side: KeyPair.Sign(); pairing.Client gains an optional keypair via
SetKeyPair(), wired in at both call sites (cmd/agent/pair.go and
internal/pairdriver/driver.go); Poll attaches the signature header.
Verified: backend 89 passed / 12 skipped (4 new PoP tests: valid sig accepted,
invalid rejected, wrong-key rejected, no-sig backward-compatible); go build
./... + go vet + go test ./... all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements a RustDesk-style short-code pairing feature across agent, backend, frontend and docs.
Key changes:
- Agent CLI: adds `serverkit-agent pair` command (agent/cmd/agent/pair.go) that generates/loads an Ed25519 keypair, enrolls with the panel, shows a rotating pair code, and saves credentials on claim. Adds passphrase marker handling and interactive/headless modes.
- Agent internals: new pairing package with client and keypair (agent/internal/pairing/*.go). Keypair files are encrypted using new exported config EncryptBytes/DecryptBytes utilities and MachineID() helper in agent/internal/config/config.go. go.mod/go.sum updated (Go version and x/term, x/sys bumps).
- Backend API: new pairing blueprint exposing endpoints for enroll, poll, code refresh/freeze, lookup and claim (backend/app/api/pairing.py). Adds PendingAgent model (backend/app/models/pending_agent.py) to track enrollments and their lifecycle.
- Backend service: pairing_service (backend/app/services/pairing_service.py) implements enrollment, rotation, long-polling, claim handling, credential minting and one-shot delivery. Adds a background pairing pruner thread to remove expired pending enrollments (registered in backend/app/__init__.py).
- Frontend: add API client support and page changes to integrate pairing flow (frontend/src/services/api/pairing.js, modifications to Servers.jsx and api index).
- Docs: adds pairing.md explaining the pairing flow.
Why: provide a secure, user-friendly mechanism for operators to claim new agents via short rotating codes and a passphrase, simplifying server onboarding while protecting credentials and preventing brute-force attempts.