The Flask middleware emitted HSTS with includeSubDomains+preload whenever not in
debug mode -- even on HTTP-only or Cloudflare-Flexible installs, where preload is
a hard-to-reverse commitment. HTTPS is intentionally optional, so HSTS is now
gated on the operator's recorded SSL choice.
- config._resolve_ssl_mode(): SERVERKIT_SSL_MODE env -> /etc/serverkit/ssl-mode
file -> safe 'insecure' default. Exposed as SSL_MODE / HSTS_ENABLED.
- security middleware sends HSTS only when HSTS_ENABLED and not debug. Behind a
proxy Flask can't distinguish real TLS from a Flexible edge via
X-Forwarded-Proto, so we trust the installer's choice, not the request scheme.
- install.sh records SERVERKIT_SSL_MODE in .env; existing installs are covered by
the /etc/serverkit/ssl-mode file. The nginx edge still emits HSTS in its own
secure server block, so secure installs are unaffected.
Proving tests in tests/test_security_headers.py. Full backend suite: 342 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a cert is issued for a domain managed by a connected DNS provider,
ServerKit now auto-creates a `CAA 0 issue "letsencrypt.org"` record at the
zone apex -- pinning issuance to our CA and clearing the "CAA not enabled"
scanner finding. Degrades to manual instructions when no provider manages the
domain; never fails issuance on a CAA hiccup.
- DNSProviderService.ensure_caa_record(): high-level helper mirroring
ensure_a_record (find managing zone -> upsert apex CAA -> degrade to manual).
- parse_caa_value(): BIND 'flags tag "value"' -> Cloudflare/DO data object.
- Cloudflare set_record now sends the structured `data` object CAA requires
(a flat `content` string is rejected), and matches on tag+value so it does
not clobber a different CA's existing CAA record.
- DigitalOcean set_record sends flags/tag for CAA too.
- DNSZoneService._cloudflare_sync uses structured data for CAA, so the preset
CAA record syncs correctly to Cloudflare-backed zones.
- ssl_service.obtain_certificate calls the hook and surfaces the result.
Proving tests in tests/test_dns_caa.py (7 cases, all green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KIMI's optional-SSL commit hardened ssl_protocols/ssl_ciphers per-vhost but
left the system nginx.conf http{} defaults untouched, so the default server
and any non-ServerKit vhost could still negotiate TLS 1.0/1.1 and weak ciphers
(the scan's "insecure TLS versions" / "weak ciphers in TLS 1.2" findings).
- install.sh: harden_global_tls() rewrites (or injects) ssl_protocols and
ssl_ciphers in /etc/nginx/nginx.conf to TLS1.2/1.3 + AEAD-only. Editing the
line in place avoids nginx's duplicate-directive error on Debian/Ubuntu,
which already set ssl_protocols in http{}.
- update.sh: re-applies the floor so existing installs harden on upgrade too.
- Persist the panel domain to /etc/serverkit/panel-domain; update.sh recovers
it from there (or the live cert path) instead of the commented-out .env
SERVERKIT_PUBLIC_URL grep, which never matched -> secure installs were
getting the YOUR_DOMAIN placeholder re-applied and nginx -t failing on update.
- update.sh now degrades to the HTTP config when a secure cert is missing,
rather than leaving nginx unable to reload.
Global TLS settings are inert in HTTP-only mode, so optional SSL is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add secure nginx config with TLS 1.2+, HSTS preload, CSP, and security headers
- Add insecure HTTP fallback config for users who cannot or do not want HTTPS
- Make installer best-effort SSL: try Let's Encrypt, fall back to HTTP without failing
- Preserve secure/insecure SSL choice across updates via /etc/serverkit/ssl-mode
- Support SERVERKIT_SKIP_SSL=1 to disable HTTPS entirely
- Add CAA record to default DNS zone template
- Harden generated per-app nginx configs with HSTS preload, CSP, and security headers
- Align Flask security middleware headers with nginx edge config
Reviewer-facing summary of the new endpoints, pages, models, config flags,
and operational notes for the DDNS / image-update / auto-sleep / auto-scale /
GPU / backup-encryption / WAF features and the layout switcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- API client: WAF methods (getWafPolicy/updateWafPolicy/applyWaf/
getWafEvents/getWafStatus/installWaf) in containerOps.js.
- WafPanel.jsx: install banner (+ install action), mode (off/detect/block)
with descriptions, paranoia level + anomaly threshold, a disabled-CRS-rule
chip editor, Save (saves + applies) + Re-apply (surfaces manual_include
when no vhost is found), and a recent-events table.
- ApplicationDetail: a "WAF" tab (nginx-served apps), wired like Container Ops.
Build passes; new file lint-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- backup_crypto.py: Fernet file encrypt/decrypt using the existing
SERVERKIT_ENCRYPTION_KEY (no new deps) — encrypt_file (-> .enc),
decrypt_file, is_encrypted_backup, encryption_available.
- BackupService: an opt-in `encrypt_backups` config flag; _finalize_backup
encrypts each artifact before _auto_upload (so the stored/remote copy is
encrypted) and records encrypted/path/size; restore_application and
restore_database transparently decrypt a .enc backup to a temp file first.
- 9 tests: byte-exact roundtrip, wrong-key failure, availability reflects
env, is_encrypted_backup, and an encrypted backup_files create+restore.
Notes: in-memory (streaming large archives is a follow-up); key loss =
unrecoverable backups (back the key up out of band). Additional remote
targets (WebDAV/Azure/Dropbox/SFTP) and restore drills remain follow-ups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ContainerScalePolicy model (per-app: enabled, service_name, min/max
replicas, cpu high/low %, cooldown, current_replicas).
- ContainerScaleService: evaluate() scales a compose service up/down by one
replica when average CPU crosses the high/low thresholds, within bounds and
outside the cooldown; scale_to() for manual; sweep() for cron. Uses
`docker compose --scale`; local + scale-capable services only.
- apps.py: GET/PUT /<id>/scale-policy, POST /<id>/scale,
POST /<id>/scale/evaluate, POST /scale-sweep (admin).
- 10 tests: bounds clamping, scale up/down, hold-at-max, cooldown, disabled,
unknown-cpu, manual endpoint, auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GpuService shells out to nvidia-smi for per-GPU stats (utilization, VRAM +
memory %, temperature, power, fan, driver) and GPU compute processes with
best-effort PID->container resolution via cgroup. Coerces [N/A] to null and
degrades to "no GPUs" when nvidia-smi is absent.
- GET /api/v1/gpu/ returns {available, gpus, processes}.
- 6 tests: row parse, N/A coercion, availability, no-GPU info, endpoint, auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ContainerSleepPolicy model (per-app: enabled, idle_timeout_minutes,
last_activity_at, asleep) + a sleep badge in Application.to_dict.
- ContainerSleepService: sleep/wake an app (compose_down/up or
stop/start_container, local), record_activity, and sweep_idle() which
stops enabled+awake apps idle past their timeout — and never sleeps an app
with no activity baseline.
- apps.py endpoints: GET/PUT /<id>/sleep-policy, POST /<id>/sleep,
POST /<id>/wake, POST /sweep-idle (admin, cron-drivable).
- 9 tests: policy, sleep/wake, docker-failure, remote guard, sweep
selection, no-baseline skip, endpoints, auth.
Follow-ups: traffic-based idle detection (feed record_activity from the nginx
log) and request-triggered wake-on-demand; remote-server sleep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- POST /api/v1/apps/<id>/image-update/apply pulls the newest image for a
compose-managed app, then recreates it (compose_pull + compose_up),
reusing the existing local/remote deploy path (DockerService /
RemoteDockerService) exactly like start_app. Refreshes the image_update
badge afterward.
- Guarded to docker-compose apps; a standalone container would need its full
run spec to recreate, so those get a clear "update manually" message.
- 4 tests: pull-then-recreate ordering, pull-failure propagation, non-compose
guard, auth gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ImageUpdateCheck model + ImageUpdateService: compare an app's local
RepoDigest against the registry's current index digest (via
`docker buildx imagetools inspect`) for the same tag, and flag
update_available when they differ. Shells out to docker like the rest of
the Docker layer; degrades gracefully when the image is local-only or the
registry is unreachable.
- API: admin POST /api/v1/image-updates/applications/<id>/check and
GET /api/v1/image-updates/applications/<id> for the latest result.
- Application.to_dict gains a lightweight image_update badge (mirrors the
existing image_scan badge).
- 7 tests: digest diff/match/unknown, no-image, badge, endpoint, auth gate.
One-click apply (pull + recreate) to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DdnsHost model: a hostname bound to a DNS zone plus a secret token; the
token is masked in API responses except when first minted.
- DdnsService.update_ip reuses DNSZoneService so a configured provider
(e.g. Cloudflare) is synced automatically; chooses A vs AAAA by IP family;
no-op when the IP is unchanged.
- API: admin CRUD for hosts + a public, token-authenticated /ddns/update
endpoint that a router or cron calls when its IP changes (IP from ?ip= or
the request source). Wrong token -> 401, bad IP -> 400.
- 8 tests covering the service flow and both endpoints.
UI panel to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add 'topbar' to LayoutContext and a third switcher button (Sidebar |
Compact | Top bar).
- Topbar: the sidebar becomes a horizontal bar pinned to the top; top-level
items lay out in a horizontally-scrollable row with an underline active
indicator. Category headers, expand chevrons, and sub-items are hidden
(sub-nav is reached from each page's in-page navigation), which sidesteps
the horizontal-overflow vs. dropdown clipping trap. Desktop-only; the
mobile drawer is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New LayoutContext (localStorage-persisted, sets data-layout on <html>),
mirroring ThemeContext.
- "Layout" switcher in the sidebar user menu: Sidebar | Compact.
- Compact rail: a 64px icon-only sidebar that expands to the full sidebar
on hover/keyboard focus; desktop-only, the mobile drawer is unchanged.
- Reuses existing nav markup — labels hide via font-size:0 (kept in the
accessibility tree); no nav-render changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Auto-generate SERVERKIT_ENCRYPTION_KEY in production when missing.
- Detect canonical domain from Host/X-Forwarded-Host; persist via SystemSettings.
- Expand CORS origins dynamically to include the canonical domain.
- Add SystemTab UI to view/save detected panel domain and HTTPS preference.
- Install script writes key, prompts for PANEL_DOMAIN, and sets nginx IP→domain redirect.
- Add backend tests for key bootstrap and domain detection helpers.
- Add webauthn dependency.
- Add PasskeyCredential model and Alembic migration 019_passkey_credentials.
- Add PasskeyService for registration/authentication challenge flow.
- Add auth API endpoints:
- POST /auth/passkeys/options/register
- POST /auth/passkeys/register
- GET /auth/passkeys
- DELETE /auth/passkeys/<id>
- POST /auth/passkeys/options/authenticate
- POST /auth/passkeys/authenticate
- Expose passkey_enabled flag in User.to_dict().
- Add ImageVulnerabilityScan and SbomArtifact models.
- Add ImageScannerService to download/install grype and syft into
/var/lib/serverkit/scanners, run CVE scans, generate SPDX SBOMs,
and enforce a configurable deploy gate.
- Add security API endpoints for scanner install, image scans,
scan history, deploy-gate checks, and SBOM create/download/list.
- Include lightweight image_scan badge in Application.to_dict() for UI.
- Add Alembic migration 018_image_scan_sbom.
- Add SERVERKIT_ENCRYPTION_KEY generation in install.sh.
- Ensure SERVERKIT_ENCRYPTION_KEY exists during update.sh runs.
- Encrypt/decrypt SSO client secrets, source GitHub client secret, and
AI API key in SettingsService transparently.
- Mask secret values in SettingsService.get_all() and
get_all_with_metadata() so the API never returns plaintext.
- Add SettingsService.migrate_legacy_secrets() and invoke it at startup
alongside DNS/storage/cloud legacy-secret migrations.
Simplify and clarify the MSI build comments in agent/packaging/msi/Product.wxs. Replace explicit dotnet tool/install commands with concise guidance to install WiX Toolset v5 and the matching Util extension (pinned to 5.0.2), and add a reference to agent/packaging/msi/build.ps1 for the exact CI commands. This makes the instructions shorter and points maintainers to the CI script for reproducible steps.
Use the correct WiX extension version specifier (wix extension add --global WixToolset.Util.wixext/5.0.2). Updated the GitHub workflow, MSI Product.wxs comments, and PowerShell build script message to use the slash-style version token so the extension installs correctly with wix v5.
Refactor agent GitHub Actions and build tooling to separate concerns: add a dedicated build-ui job (upload/download agent-ui-dist) and a version job to resolve the release version; make downstream jobs depend on these outputs. Update workflow steps to consume artifacts/outputs (use needs.version where appropriate), and mark multi-platform jobs to not rebuild the UI. Add Makefile UI targets (build-ui, UI_DIST dependency, cleanup) so go:embed succeeds locally. Improve Windows MSI docs and build script: require specific wix tool/version and verify WixToolset.Util extension before building.
Add a dedicated Node.js build stage to the agent Dockerfile to compile the desktop UI and embed its output into the Go binary. Copy the built UI into internal/agentui/dist before go build and make the Go build respect TARGETOS/TARGETARCH args for multi-arch builds. Also update agent/.dockerignore to exclude dist/, ui/dist/, and ui/node_modules/ to keep the image build context small and cache-friendly.
Switch MSI packaging to WiX Toolset v5: update CI to install the WiX Util extension, update Product.wxs comments and namespaces for v5 and show using the Util extension at build time, and enhance build.ps1 to pass the extension to wix build, fail loudly on non‑zero exit code, verify the MSI output exists and print the resolved output path. These changes ensure builds work with WiX v5 and provide clearer errors/output.
- Extend Application model with source, compose_file, systemd_unit,
managed_by, version, and upload_path columns.
- Add migration 017_manual_apps for the new columns.
- Add POST /apps/manual to register existing server paths/systemd units.
- Add POST /apps/upload with versioning, rollback, and .env preservation.
- Update DockerService compose helpers to accept a custom compose_file.
- Update ProcessService to query systemd units and compose projects.
- Update apps API start/stop/restart/status/delete to honor compose_file
and avoid deleting unmanaged manual paths.
- Add Manual/Local and Upload ZIP source modes to NewService.
- Show source badges in Services list and disable Git controls for
manual/upload apps in ServiceDetail.
- Add version list, rollback, and new-version upload UI for upload apps.
The previous commit added a module-level `from sqlalchemy import event` to
register SQLite speed PRAGMAs. But the test-system-utils workflow runs
tests/test_utils_system.py with only `pip install pytest` (no SQLAlchemy), and
pytest still imports conftest.py — so it broke with
ModuleNotFoundError: No module named 'sqlalchemy'.
Wrap the import + listener in try/except ImportError. Those jobs run no
DB-backed test, so skipping the PRAGMA tuning there is harmless; the full
backend job still gets it.
Verified: tests/test_utils_system.py (41 passed) and the agent e2e roundtrip
both pass; conftest now imports with just pytest + stdlib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_command_roundtrip_via_send_command drives agent_registry.send_command
from a background thread while the main thread handles /poll and /result.
Flask-SQLAlchemy serves :memory: from a single shared connection (StaticPool),
so the two threads issue interleaved transactions on one connection and the
background commit intermittently dies with ObjectDeletedError /
PendingRollbackError (it passed some CI runs, failed others).
Point tests at a temp-file SQLite instead: each thread gets its own connection
with SQLite's file locking, which is safe and deterministic. Add
synchronous=OFF / journal_mode=MEMORY PRAGMAs so the file DB stays ~as fast as
in-memory (throwaway data needs no durability). No production code changed.
Verified: the e2e roundtrip passes 22/22 consecutive runs (was flaky); full
suite green (exit 0, 232 passed / 12 skipped locally).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
save_config() ran os.makedirs(SERVERKIT_CONFIG_DIR) unconditionally rather
than the directory of the file it actually writes (cls.CONFIG_FILE). When a
caller overrides CONFIG_FILE (test_storage_secret_encrypted_at_rest points it
at a pytest tmp dir), creating the real config dir failed on the sandboxed CI
runner and — because the surrounding except swallows it and returns
{success: False} — the file was never written, surfacing as a confusing
FileNotFoundError. Now makedirs(dirname(CONFIG_FILE)); production behavior is
unchanged (there dirname(CONFIG_FILE) == SERVERKIT_CONFIG_DIR). 8/8 encryption
tests pass; full backend suite was 241 passed / 1 failed -> now green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Safety dependency scan flagged PyJWT==2.12.1 (5 vulns) and
cryptography==46.0.7 (1 vuln, bundled-OpenSSL). Bump to the Safety-suggested
fixes, which resolve all 6 currently-active (non-policy-ignored) findings.
Verified: installs with no resolver conflicts, the Fernet/encryption tests
pass, the app boots (JWT manager), and the full suite is 232 passed / 12
skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Backend CI installed only requirements.txt (which doesn't carry the
test-only pytest dependency) and then ran `python -m pytest`, failing with
"No module named pytest". Add `pip install pytest` to the install step.
pytest is the only test-only import across tests/ (everything else they
import — cryptography, flask_jwt_extended, prompture, yaml — is already a
runtime dependency). Full suite: 232 passed, 12 skipped locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agent embeds its desktop console via //go:embed all:dist
(internal/agentui/embed.go), which needs agent/internal/agentui/dist to
exist at compile time. The CI and release workflows ran go build/vet/test
without first building the React app (agent/ui -> vite build outputs to
that dist), so a fresh checkout failed with
"pattern all:dist: no matching files found".
Add a Node setup + `npm ci && npm run build` (in agent/ui) before the go
steps in both jobs of agent-ci.yml and in the release build job. Verified
the build command produces internal/agentui/dist locally.
Not a Go-version issue: wireguard-go@2026, gvisor@2025 and the agent all
declare go 1.23.1, and gvisor's gohacks build constraints are go1.20+
(no upper bound), so CI's Go 1.23 compiles them fine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>