Faro access grants install keys through the agent's ssh:keys
actions. A locked-down server had no canned profile for that short
of Full Access; ssh_access grants ssh:keys:* plus the health reads.
The SSL-enable and server-overview changes put session and query
calls in the routes, tripping the controller-boundary ratchet. Move
them into SiteDomainService and WorkspaceService, raise a typed error
for bad allowed_ips, point the ACME-contact test at the new vhost
door, and regenerate the baseline and inventory (476 -> 453).
POST /servers/register needs no login and ran a slow PBKDF2 check
against every pending server per request, so a few IPs under the
5/min limit could keep the single worker's CPU busy. Tokens carry 256
random bits, so store a SHA-256 digest and find the row directly;
hashes issued before this still verify until they expire.
Every subscriber starts its own docker logs process but emitted into
the shared app room, so with two viewers open each saw every line
twice, got the other's tail replayed, and both saw 'ended' when either
stream stopped. Deliver to the owning socket while room membership
still gates it, keeping the per-line access re-check.
redeem() checked used_at, then set it and committed, so two requests
arriving together (a mail-preview bot and the user, or an attacker
racing them) both passed the check and both got a session. Claim the
link with one conditional UPDATE and refuse when no row changed.
docker exec -e MYSQL_PWD=<pw>, mysqldump -p<pw> and friends put the
root DB password in argv, readable by every local account through ps
or /proc. Add secret_env() and hand the password to the child's
environment (bare -e NAME for docker exec), and drop it from the
mysqladmin readiness ping, which never needed it.
Both files used a truncating open() and their loaders read damage as
empty, so a full disk mid-write reset off-site backups to local and
the next cron add replaced every job's metadata with the new job.
Route both through the config store's temp+replace write; an
unreadable file is moved aside instead of being overwritten.
cron treats a bare % as end-of-command and pipes the rest to stdin,
so a job like 'date +%F >> log' ran as 'date +' on schedule while
Run now (bash -c) ran it correctly. Write lines with \%, unescape
when parsing, and locate a job's line by parsing in edit and
tracking so legacy unescaped lines are still found and fixed.
Enabling SSL on a custom domain edited the vhost in place, but every
later rewrite (deploy, slot switch, domain attach, drift repair,
restore) rebuilt it from Domain rows that only knew wildcard certs,
so HTTPS silently disappeared while the row still said enabled.
Rebuilds now resolve each domain's own cert and render one server set
per certificate when domains differ. Enable/attach publish through
write_app_vhost and only claim SSL when nginx accepted it, the key
path is stored under its real column (legacy rows fall back to
certbot's privkey.pem), disable drops the HTTPS block, and a repeat
add_ssl_to_site swaps the cert instead of breaking nginx -t.
Every emit re-validated the socket's saved JWT including exp, and
access tokens last 15 minutes, so metrics, terminals, logs and
notifications were cut a quarter-hour after connecting; clients do
not reconnect after a server-side disconnect. Re-checks now skip exp
only; revocation, auth_version and deactivation still end the socket,
and connecting still needs an unexpired token.
pg_create_database/user and pg_grant/revoke built superuser SQL from
raw names and passwords, unlike the MySQL helpers beside them. A quote
in a password broke CREATE USER, and the create routes ignored that
and returned credentials for a user that did not exist. Validate
identifiers and privilege lists, escape the password, and surface a
failed user as user_error instead of fake credentials.
PROTECTED_ROOTS only matched the path the user typed, so copying /opt
copied /opt/serverkit/.env (JWT key, DB creds) somewhere readable,
and deleting or moving /opt took the running panel with it. Refuse
copy, move, rename and delete when a protected root lies inside the
source, and keep symlinks as links when copying a tree.
find_or_create_user attached a new SSO identity to any user with the
same email, but Google and generic OIDC profiles never carried
email_verified. An IdP account showing an unverified copy of the
admin's address signed in as the admin. Carry the claim through and
refuse to link without it; GitHub and SAML already vouch.
Auto-provision defaulted on with role developer and no domain filter,
so enabling GitHub login for yourself let any GitHub user sign up and
open remote terminals, while password registration stayed closed.
Default to off and viewer; turning it on stays an explicit choice.
Only three routes checked can_access_server; the rest loaded the server
by id, so a developer outside a workspace could run cron, write files,
install packages or widen agent permissions on its servers, and the
list/overview/docker picker showed every server. Every <server_id>
route now resolves through the workspace check, lists filter in SQL,
and PUT /servers/<id> validates allowed_ips like PUT /allowed-ips.
A sweep test walks the URL map so a new unguarded route fails CI.
check_failed_logins computed a cutoff from since_hours and never used
it, so the Security -> Events "Failed login attempts (24h)" card and
its alert counted every failure still in /var/log/auth.log or
/var/log/secure: up to a week of SSH attempts under the usual weekly
log rotation.
Each matching line is now dated from its stamp, either RFC 3339
(rsyslog's high-precision format) or the traditional BSD syslog stamp
(no year, so a December line read in January is placed in the previous
year), and lines older than the cutoff are skipped. A line whose stamp
is not recognised is still counted rather than hidden. The log paths
move to SecurityService.AUTH_LOGS so the tests can point at a temp
file.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Templates page only featured a hard-coded list, so `featured: true`
in a template (Prompture Hub, Torre) never reached the Featured-first
view. The page now honors the flag alongside the list, and bundled
templates carry it into the catalog and the published index.
Stop now persists the tracking flag, and the save created the data
directory unguarded. Where that fails (an unprivileged host or CI
runner) the save is now logged and reported false, as its docstring
already promised, instead of raising.
Resuming uptime tracking at boot read the history through a helper
that first created /var/lib/serverkit. The image runs unprivileged, so
that makedirs raised and the app never started. Reading now creates
nothing, and a failed resume is logged instead of aborting boot.
--version, which, postconf, pip list/freeze and mailq had opted out of
the run_checked default with timeout=None, so a wedged binary held a
request thread forever. Installs, migrations, dumps and restores keep
no timeout.
A function-local import of SlotDeployService further down
stop_application made the name local to the whole function, so the
compose-slot branch raised UnboundLocalError before reaching it: Stop
on such an app always failed. The module already imports the class.
Both used psutil without importing it, so every call has been a 500
since they were written. Nothing calls them: the process list and
service status come from /processes and /processes/services.
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.
GitHub release resolution and the SSRF-guarded download (redirect
re-validation, byte caps, token scoping) were 250 lines of
plugin_service unrelated to installing. plugin_service re-exports the
names its own code calls, so existing monkeypatches on it still land;
tests that exercised the fetch internals directly now target
plugin_fetch.
template_service.py held port allocation, magic-variable resolution and
the template catalog alongside install/update in one 2,800-line class.
Those three groups move verbatim into TemplatePortsMixin,
TemplateMagicMixin and TemplateCatalogMixin, each in its own module;
TemplateService inherits them, so its surface (106 attributes, same
classmethod/staticmethod kinds) and every caller are unchanged. Tests
that patched module globals now patch the module the code lives in.
The value went straight to a privileged journalctl -p. As its own argv
element it could not inject options, but anything other than levels
0-7 now gets a 400 instead of a journalctl error.
ps, inspect, info, version, tag and the network/volume inspects ran
with timeout=None, so a wedged dockerd held every request thread that
asked, forever; the panel runs a single worker process. They now give
up after 30s, and the Containers page reports Docker unreachable.
Pull, build, logs and compose stay unbounded on purpose.
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 cited plan sections as "(§G3)" or "(§8 unification)", which
point into the same git-ignored plans the plan-number citations did.
References next to a real document (docs/REDESIGN_MAP.md §3) stay. Code
and runtime strings are unchanged (AST and token comparison).
The worst cases the audit named: docstrings and header comments that
told how three old copies drifted, or quoted subprocess counts that
were stale the day they were written, instead of stating the contract.
Each now says what the function does and keeps one line of why. Also
drops the always-zero n_cloud counter that existed only to keep a boot
log line's or-chain intact.