The supported-versions table still said 1.6.x while 1.7.112 ships; the
SECURITY_AUDIT.md link pointed at a file that has never existed in git
history. Adds a short API Authentication section describing the JWT-only
default with per-route API-key opt-in through the rbac policy decorator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the Go agent moved out of this monorepo into jhd3197/serverkit-agent, the
installers did not follow it. They still asked jhd3197/ServerKit for `agent-v*`
tags -- coordinates that exist in no repository: jhd3197/ServerKit has zero
agent-v* releases, and the binaries are published as vX.Y.Z in the agent repo.
Version discovery therefore failed outright, and the download URL was a 404.
Everything downstream was already correct and is unchanged: the asset name
serverkit-agent-<ver>-linux-<arch>.tar.gz, the entry inside the tarball
(serverkit-agent-linux-<arch>) and the checksums.txt entry names all match the
real v1.2.0 release. Only the repo and the tag prefix had rotted.
Version discovery now requires a digit after the "v". The agent repo carries a
malformed release tagged literally "v", which would otherwise parse to an empty
version and build .../releases/download/v/serverkit-agent--linux-amd64.tar.gz.
test_agent_install.sh encoded the dead scheme in its fixtures (agent-v0.3.2), so
it stayed green throughout. It now pins the release coordinates as a contract:
the repo, the /v${VERSION}/ URL scheme, the absence of any live agent-v
reference, and the checksums URL exercised through the curl stub. All four fail
against the pre-fix script.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
install.sh wrote no TRUST_PROXY_HEADERS line at all, so the code default (False)
applied while every host install serves the panel through its own nginx. Per-IP
rate limits, the login brute-force throttle and every audit-log entry therefore
keyed on 127.0.0.1, which quietly weakened all three -- one abusive client shared
a bucket with everyone else. .env.example claimed true, so the two disagreed and
it looked configured.
Trusting X-Forwarded-For is only safe when nothing can reach gunicorn without
traversing a proxy we control, so the switch follows that condition rather than
assuming it. On a host install it holds: render_service_unit binds gunicorn to
127.0.0.1 and bootstrap_firewall opens SSH + 80/443 + PANEL_PORT and never the
backend port. backend_bind_host() is now the single source of truth for both the
systemd unit and the trust decision so the two cannot drift, and
backend_is_loopback_only() gates the setting.
- stock install: TRUST_PROXY_HEADERS=true, TRUSTED_PROXY_HOPS=1
- SERVERKIT_EXTERNAL_PROXY=1: true with 2 hops, unchanged
- SERVERKIT_BIND_HOST not loopback: false plus a warning. The port is directly
reachable there, so the header is attacker-chosen, and a forged IP is worse
than a wrong one because it defeats the throttles that consume it.
Re-runs backfill it into installs that predate this, via _env_default_key, which
fills a gap but never overrides an uncommented value -- flipping someone's
deliberate false back on would be the worst kind of surprise.
.env.example moves to false on purpose: that file is the Docker/manual path and
docker-compose.yml publishes 5000 on all interfaces, so true was the unsafe
default there. Host installs never read it. SECURITY.md claimed "the shipped
nginx deploy sets it", which was untrue until now.
T44 covers all six behaviours, each mutation-checked by line number: disabling
the branch, inverting it to trust an exposed port, and disabling the backfill
each fail exactly one assertion and no others.
Shell suite 121 passed. Backend suite 3542 passed, 48 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plan 55 Phase 4. EXTENSIONS.md said `permissions` is "**enforced** by the SDK
gate: require_permission(slug, "docker") raises unless docker is declared".
That sentence is true about the function and misleading about the system: the
gate only fires where something calls it, and nothing routes Docker through it.
The SDK exposes no docker, shell, filesystem or network helper at all, and hands
out `db` as raw SQLAlchemy, so an extension that wants any of them imports the
host module directly and no in-process check is involved. Declaring `docker`
does not stop an extension using Docker, and not declaring it does not either.
Both documents now state which capabilities are actually mediated -- today
`agent.command:<action>` and nothing else, because the SDK is the only route to
an agent -- in a per-capability table, and say plainly what declaring does
instead: it tells the operator at install time what the extension claims to
need, and lets them refuse.
SECURITY.md gains an Extension Trust Model section next to the agent one. It
leads with the part that matters most and was never written down: an extension
is in-process code running with the panel's privileges against the panel's
database, nothing sandboxes it, and no permission string changes that. Signing
and sha256 pinning are described for what they are -- integrity of what you
installed, not containment of what it does -- along with the unsigned-prompts,
bad-signature-fails split, and the pip opt-in.
It also explains why the new detail page reports "cannot be observed" rather
than a zero count for unenforceable permissions: absence of evidence is not
evidence of absence, and a "never used" badge there would imply a boundary that
does not exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plan 55 Phase 1. The sweep found zero vulnerabilities: every raw-HTML sink is
safe by construction (escape-first SQL/file highlighters, sanitizeSvgInner for
extension icons, the escape-then-allowlist markdown renderer) and the backend
has no |safe/Markup(/render_template_string.
To keep it that way: scripts/check-html-sinks.mjs (wired into npm run lint) and a
Python mirror test (test_html_sink_sweep.py, runs in the backend suite) fail on
any raw-HTML sink that neither references an allowlisted sanitizer nor carries a
'sink-safe:' annotation. Standardized annotations on the three variable-fed sinks
(markdown, SQL editor, marketplace icon) and documented the rule in SECURITY.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
The app runs flask-socketio with async_mode='threading' (the backend
is real-thread based: jobs scheduler, queue bus, agent registry), where
engineio serves WebSocket via simple-websocket on the raw socket. The
systemd unit template and Dockerfile still launched gunicorn with the
GeventWebSocketWorker, which ALSO answers the upgrade — the client got
HTTP/1.1 101 twice and read the second one as a frame ('Invalid frame
header'), so every panel silently degraded to polling. Proven by
hexdump (double 101, first 'frame' byte 0x48 'H') and fixed live:
single 101 + 0x81 text frame, socket.io connects through Cloudflare.
- unit template + Dockerfile: gunicorn -w 1 --threads 100 (single
PROCESS stays mandatory — in-memory agent gateway)
- requirements: pin simple-websocket explicitly (was transitive-only)
- CLAUDE.md / ARCHITECTURE.md / SECURITY.md single-worker notes updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SECURITY.md: spell out that agent.key encryption is off-host/tamper
protection, not confidentiality against local root; 0600 perms are the real
control (L2).
- agent_registry.py: reword the per-message session-token "TODO" to an accepted
limitation (defence-in-depth, not a bug) (L7).
- AGENT_AUDIT.md: record H1/H2/H3 and L1-L7 status, with L4 (cap OR-merge),
L5 (single-worker), L6 (windows .bat) noted as accepted by-design/documented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dev was ~149 commits ahead of the last release with no changelog and several
stale docs. This brings the user-facing docs in line with what actually ships.
New:
- CHANGELOG.md — curates the unreleased panel work (agent fleet, plugins,
status pages, cloud provisioning, RHEL support, installer hardening).
- agent/CHANGELOG.md — the agent release workflow links to this on every
published release; it was a broken link. Now documents the agent's
independent release line.
- SECURITY.md — vulnerability reporting policy + agent trust model
(agent.key is host-equivalent; Features.Exec off by default; single-worker).
Fixes:
- Stale version stamps: docs/README.md and docs/API.md said 0.9.0 -> 1.6.7.
- Supported OS: README/INSTALLATION said Ubuntu/Debian only; installer also
covers Fedora and RHEL/Rocky/AlmaLinux.
- Dead links/placeholders: API.md wiki link, INSTALLATION.md "yourusername".
- Windows agent + short-code pairing were undocumented; added to README,
agent/README.md, and docs/pairing.md (incl. corrected host-stable
credential-key derivation — USERNAME was dropped in agent 1.6.14).
- Documented the single gevent-websocket-worker deployment constraint in
CLAUDE.md and docs/ARCHITECTURE.md.
- CONTRIBUTING.md: removed reference to a non-existent frontend test suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>