mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-20 17:43:29 +00:00
main
35 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ee227363e |
fix(agent): adoption cannot hide a node; Config Import on fresh installs (v1.11.1)
Six fixes to the agent's discovery path and one long-standing parity gap, found
by auditing it in loops against a real fleet.
DISCOVERY REPORTS THAT NEVER REACHED THE SERVER
The agent posts an unmanaged keepalived.conf for adoption and caches the hash of
what it sent, so the file - which carries the VRRP password - is re-posted only
when it changes. Delivery was judged by curl's exit code, and curl without -f
exits 0 on 5xx too, so a report the server REJECTED was recorded as delivered.
Since a hand-maintained config does not change on its own, that node dropped out
of "Unmanaged keepalived detected" permanently; the only cure was deleting a
cache file on the node by hand.
- the report is cached only on a 2xx;
- GET /agents/{name}/keepalived-config now reports whether the server actually
holds a discovery for that agent, and the cache may only suppress while it
says yes - which is what lets nodes stuck from earlier releases recover on
their own, with nobody touching them;
- the flag is parsed with has() + tostring, not `// empty`: jq's alternative
operator returns the alternative for **false** as well as null, so the naive
form could not tell "no record" from "older backend" and the recovery would
have been completely inert;
- a 400/413/422 records the refusal so identical bytes are not re-posted
forever - 4xx and 5xx agent calls are never sampled out of the request log,
so an unattended loop would write a row carrying the whole config every
cycle - while 401 and 404 keep retrying, because here they mean a token
rotation or an agent row briefly absent, not a bad payload;
- the CLEAR path had the same exit-code defect, where it left a stale row
offering a managed node for adoption with nothing to ever retry it.
CONFIG IMPORT WAS A NO-OP ON FRESHLY INSTALLED AGENTS
check_config_requests uploads a node's live haproxy.cfg on request. It was
defined in the installer body and in the self-upgrade daemon, but not in the
heredoc a fresh install writes, and its call site is guarded by `type` - so on
such a node the operator asked for a config and nothing arrived, with no error
anywhere. Any agent that had self-upgraded at least once already had it, which
is why it went unnoticed. The self-upgrade definition is copied verbatim
(verified line-for-line). A freshly installed agent now polls that endpoint once
per cycle exactly as every upgraded agent already does; no node running today
changes behaviour.
DETERMINISTIC CONFIG PATH
A pool may hold several clusters and the join that resolves keepalived_config_path
was unordered, so the path handed to an agent could differ between polls whenever
two clusters disagreed - the agent would inspect a file that is not there and the
node would never appear, intermittently. A customised path now wins over the
shipped default, then the lowest cluster id. Verified against a real PostgreSQL
over seven arrangements: with one cluster per pool, or when every cluster carries
the default, the value is byte-identical to before.
Verified end to end on a production fleet and, for each decision, against the
real _kp_discover block rather than a paraphrase.
Backend suite: 1674 passed, 152 skipped. bash -n passes on the whole file and on
the fresh-install body in isolation. The keepalived path is logic-identical
across both daemon copies, now pinned by a test.
|
||
|
|
0eb587dfa8 |
fix: keepalived validation gate and deploy acknowledgements (v1.10.13)
Two agent-side fixes found while taking the VIP adoption flow through a real
HA pair, released together.
1. A VALID CONFIG WAS REJECTED BY ITS OWN WARNING (v1.10.12)
Before writing a rendered keepalived.conf the agent validates it with
keepalived -t and, on failure, keeps the running config and does not restart
keepalived. That fail-safe is right, but it treated ANY non-zero exit as
invalid - and keepalived's config-test exit code does not separate fatal from
benign. Measured on 2.2.8:
clean config ..................... 0
auth_pass longer than 8 chars .... 5 "Truncating auth_pass to 8 characters"
missing '}' ...................... 5 "There are 1 missing '}'s"
unknown keyword .................. 5 "Unknown keyword '...'"
script without script_security ... 6 "SECURITY VIOLATION ..."
Exit 5 covers both a harmless truncation and a broken file, so a VRRP password
over eight characters was enough to block every apply - including on a node
whose own running config emits the same warning and had been serving the VIP
for weeks. Accepting exit 5 would have accepted broken configs, so the gate now
judges the OUTPUT: known-benign messages are dropped and anything remaining
still fails. It fails CLOSED - an unrecognised message, or a non-zero exit with
no readable output at all, is fatal - and the filter is an allowlist, never a
denylist. The agent also reports what keepalived said, in its log and in the
status the HA/VIP page shows; discarding it left a correct refusal that nobody
could act on.
2. EVERY DEPLOY ACKNOWLEDGEMENT WAS DROPPED
The takeover-retirement clause on POST /agents/{name}/keepalived-status reused
one placeholder for both the assignment `last_deploy_hash=$n` and the
comparison inside its CASE. PostgreSQL types a placeholder per USE, so it came
out as text in one and character varying in the other and asyncpg rejected the
statement with AmbiguousParameterError. The whole UPDATE never ran, so no
member recorded an acknowledgement: VIPs sat at SYNCING with an empty Last ack
while the nodes were verifiably running the config, and teardown acks were lost
the same way. The hash now has its own placeholder, compared only against the
column.
Verified against real keepalived and a real PostgreSQL rather than by
inspection, including on busybox and bash 3.2, and a test asserts every $n in
those statements is bound exactly once.
|
||
|
|
a87994e06a |
fix(vip): refuse adoption that strands a node or normalises a peer (v1.10.9)
Three findings from a second pass over the adoption flow, all of the same class: something real leaving the set silently. 1. STRANDING. _collect_instance_participants can only match a node it can READ, that is ENABLED, and that is in the SAME pool. Each of those is a door a genuine member of the VRRP group leaves through without a word, and the nodes that remain are rewritten while it keeps serving the same address from an unmanaged config. Found on a live pool: one node of a pair had an unclosed vrrp_instance block, so it parsed to nothing while its partner parsed cleanly. Rather than guard each door, ask the question directly: does any reported keepalived.conf mention THIS virtual address without being one of the nodes we are about to adopt? Refuses naming the node and the reason. Scoped on the address so an unrelated file elsewhere cannot block every adoption, and excluding nodes already under management (a standing VIP, or our ownership marker) because those are not stranded. 2. SILENT NORMALISATION. prefix_length, unicast/multicast mode, HAProxy tracking and the VRRP password are stored ONCE on the VIP and re-rendered onto EVERY member, so whichever node was clicked imposed its settings on the others. prefix_length is the sharpest: the design refuses to GUESS a netmask for a live VIP, and copying one node's netmask onto another is that same change wearing a different hat. All four must now agree, with both values named in the refusal. The VRRP secret is compared by decrypting each node's token - Fernet is non-deterministic, so ciphertexts cannot be compared - and a token that will not decrypt is an error rather than an assumed match. 3. THE TAKEOVER AUTHORISATION WAS NOT ONE-SHOT. takeover_expected_hash is the permission to overwrite a keepalived.conf that lacks our ownership marker. It was written at adoption and never cleared, so it stayed valid for that file content indefinitely: restoring the pre-adoption file would have been overwritten again with no fresh human approval. It is now retired when a member acknowledges our rendered config, gated on the acked hash matching applied_config_hash so a partial or failed deploy never drops it and leaves the VIP unable to converge. The panel applies the stranding rule too, so the Adopt button is disabled with the reason instead of letting the operator click into a 422. No schema change, no agent change, no API-shape break. Backend suite: 1366 passed, 152 skipped. Frontend build clean. |
||
|
|
7dfd31832a |
feat(vip): store the keepalived.conf an agent finds on a node
Ingest side of adopting an existing VIP. A node reports the keepalived.conf it found and does NOT own to a new endpoint, and the finding is kept in a new vip_discoveries table (one row per agent, since the file is per-node). Reporting is read-only on the node. The heartbeat cannot carry this: it has the VIP address and a best-effort MASTER/BACKUP, while rendering a node's config needs eleven fields, so the file itself has to be read and parsed server-side - parsing keepalived's block syntax in bash is not something to attempt on a production load balancer. Secrets are split at ingest. The reported content may contain the VRRP auth_pass, so the password is Fernet-encrypted into its own column through the same key path as vip_instances, and the stored copy of the file has it masked. Nothing readable through the API, the UI preview or a database dump carries it in cleartext, and the parse result is never logged. A file that does not parse records its error rather than failing the agent's poll loop, and a report of "the file is gone" clears the row so the UI stops offering a stale candidate. The keepalived-config delivery gains allow_takeover and takeover_expected_hash. The agent refuses to overwrite a keepalived.conf without our ownership marker, which is the guard that protects a hand-maintained setup - and is exactly the guard adoption has to pass. Rather than weaken it, an adopted VIP authorises exactly ONE takeover of exactly the file that was analysed by pinning its md5, so a config edited between adoption and Apply is still refused instead of being silently overwritten. SCHEMA_VERSION 10 -> 11 for the new table and two additive columns. Nothing existing is altered, but the bump re-seeds the four built-in roles, which the upgrade notes call out. |
||
|
|
9e5185c458 |
fix(security): post-review hardening — agent-inventory regression, coverage gaps, SSRF newNonce
Follow-up to the RCE/missing-auth/SSRF remediation, from a thorough multi-lens
review (3 agents + a black-box audit of all 201 routes). Backend-only; no
agent-script changes.
Regression fix (introduced by the previous commit):
- GET /api/agents was made JWT-only, but deployed agents call it WITH X-API-Key
(not a JWT) to read their applied_config_version and avoid re-applying config on
restart. It now accepts EITHER a valid operator JWT OR a valid agent X-API-Key,
so agents no longer get 401 (which caused a spurious HAProxy reload every restart).
Completeness (GHSA-3p5c siblings the first pass missed — same data class, now JWT):
- dashboard.py: GET /api/haproxy-cluster-pools/{id}/agents (full agent inventory —
a direct anonymous bypass of the GET /api/agents lockdown), /api/pools,
/api/haproxy-cluster-pools, /api/dashboard/stats, /api/dashboard/overview
(auth was optional -> leaked stats/names/health/alerts anonymously),
/api/haproxy/stats.
- waf.py: GET /api/waf/rules. health.py: GET /api/health/errors.
- agent.py: GET /api/agents/generate-uninstall-script/{platform} (agent-management
endpoint; was anonymous) now requires JWT or agent key, like generate-install-script.
- config.py: POST /api/config/{validate,optimize,templates/{id}/generate} were
optional-auth (logging only) and run a HAProxy validator on caller input; now
require a JWT. (bulk-create, parse-bulk, diff and configuration/request were
already mandatory-auth — verified.)
All newly-gated endpoints are frontend-only (axios sends the JWT) or unused;
agents never call them.
SSRF (GHSA-3vh4) gap:
- acme_service._get_nonce fetched directory['newNonce'] (from the attacker-
influenceable directory JSON) with a bare session, http allowed, dual-stack, and
BEFORE the guarded _signed_request POST. Now guarded (assert_public_url +
safe_connector + no redirects + timeout), matching the other ACME sinks.
Correctness:
- Three agent webhooks (config-applied, config-validation-failed, config-sync)
swallowed their auth 401 into a 200 error body via a bare `except Exception`.
Added `except HTTPException: raise` so the 401/403 propagates.
Audit result (live black-box, all 201 routes probed unauthenticated): no data
leak and no unauthenticated mutation anywhere; every sensitive route returns
401/403 (a pre-existing group of read handlers wraps the 401 into a 500 via a
broad except — no data is exposed; left as-is, documented as cosmetic).
Verified: full pytest tests/ (1145 passed, 0 failed; +16 regression tests) + live
localtest stack smoke — agent-key GET /api/agents=200, anonymous=401, all newly
gated endpoints reject anonymous and admit JWT, the 3 webhooks return 401.
|
||
|
|
520b69a1c6 |
fix(security): remediate RCE, missing-auth and SSRF advisories (backend-only, no agent changes)
Addresses three reported advisories, all verified against the code. Fixes are
entirely server-side — deployed agents already send a valid X-API-Key on every
call, so enforcing it does not require any agent-script change or upgrade.
GHSA-7rhv-c5pc-69r8 (CRITICAL RCE — agent script-template poisoning):
- POST/GET /api/agents/script-templates/{platform} now require the agents.version
permission (was authentication-only), matching POST /versions. Blocks a viewer
JWT from overwriting the root install/upgrade script.
GHSA-3p5c-m5m4-mjpx (missing authentication):
- Agent data-plane endpoints now REQUIRE a valid X-API-Key (was optional/skipped
when the header was absent), checked before any DB access: config,
ssl-certificates (private keys!), upgrade-status, heartbeat (by-name and the
previously auth-less by-id), configuration pending-requests. Removes keyless
heartbeat spoofing and keyless rogue-agent auto-registration.
- Operator/UI endpoints now require a JWT: GET /api/agents, the entire
/api/dashboard-stats router, /api/health/{deep,agents,clusters}, and
/api/ssl/certificates/{id}/config-versions. The simple /api/health liveness
probe stays public. Adds shared auth_middleware.require_authenticated_user.
GHSA-3vh4-gvxx-wm2p (SSRF via ACME directory_url):
- New utils/ssrf_guard.py (https-only + public-IP-only, IPv4-pinned, no redirects),
applied to settings test-connection, acme_service.get_directory and
_signed_request, and validated at Let's Encrypt account creation. The
test-connection response no longer reflects arbitrary upstream JSON keys
(information-disclosure oracle) — only fixed ACME field names.
Verified: full pytest tests/ (1128 passed, 0 failed) + live localtest stack smoke
(valid JWT/key paths return 200/404 as expected; anonymous requests 401; SSRF to
metadata/private/loopback refused). No changes to backend/utils/agent_scripts/*.
|
||
|
|
23257b02cf |
perf(api): opt-in uvicorn workers + heartbeat query consolidation (v1.8.6, Issue #35)
A user running the API on a 2-core/4GB host reported slow-feeling API responses (Issue #35 follow-up). Review of the hot paths found no pathological defect; the dominant factors are the single uvicorn worker (one core serves all requests) and the constant agent-poll baseline (4 requests per agent every 30s). Two zero-risk improvements: - backend/Dockerfile: CMD now honors UVICORN_WORKERS, falling back to WEB_CONCURRENCY and then 1. Flagless uvicorn natively honors WEB_CONCURRENCY, so the fallback keeps any deployment that relied on it byte-for-byte compatible; with the final default of 1 worker uvicorn runs in-process exactly as before. >1 enables the multiprocess supervisor so multi-core hosts can use all cores. Background tasks are already multi-replica safe (FOR UPDATE SKIP LOCKED / advisory locks), as exercised by the k8s HPA deployment (2-10 replicas). `exec` keeps uvicorn as PID 1 (clean SIGTERM, verified ~1s docker stop with 2 workers). docker-compose.yml passes UVICORN_WORKERS through as empty-when-unset so a user-set WEB_CONCURRENCY is never overridden; .env.template documents it. - routers/agent.py heartbeat (by-name endpoint): the agent's status/version/upgrade_status were read with three separate single-column SELECTs against the same row; now one SELECT. Identical values and None semantics (single consistent snapshot instead of three reads); saves two round-trips per heartbeat per agent every 30s. The legacy by-id heartbeat endpoint is untouched; the heartbeat API contract is unchanged for agents of every version. - README: new "Performance Tuning" section (worker/replica scaling, and how to use the X-Response-Time header plus "Slow request detected" logs to pinpoint slow endpoints). Verification: full backend suite in docker green (1063 passed, 151 skipped; also re-run by the runtime image build); worker-count expansion matrix (unset->1, UVICORN_WORKERS=2->2, WEB_CONCURRENCY=3->3, both->UVICORN_WORKERS, empty->fallback) all correct; default run confirmed single-process with uvicorn as PID 1 and healthy API; UVICORN_WORKERS=2 confirmed parent + 2 workers, healthy API, clean shutdown; live heartbeats verified for register + existing-agent paths AND degraded agents (no stats socket / haproxy stopped / garbage stats CSV / unknown backend in server_statuses): all return 200, agent row updates correctly, zero backend errors. No schema, API, or agent changes. |
||
|
|
27fbe48c4b |
fix(agent): tolerate empty system_info in heartbeat JSON (v1.8.3)
A self-hosted agent could fail every heartbeat with HTTP 400 `Invalid JSON: Expecting property name enclosed in double quotes` when the system-info block it collects came back empty on an unusual host. The agent builds the heartbeat JSON as text, so an empty `$system_info` collapsed the `$system_info,` line to a bare comma and broke the payload. - Agent script (linux + macos, kept in sync): guard the fragment-form register_agent and send_heartbeat builders so an empty system_info falls back to a valid key and can never emit a bare comma. Uses the most portable bash glob test (no POSIX class / pattern substitution; verified on bash 3.2-5.2 and on Ubuntu/Debian/Rocky/Alpine/Amazon Linux). True no-op for healthy agents. - Backend heartbeat endpoint: parse the body as-is first and only run the malformed-JSON repair when parsing fails, so a valid heartbeat from any agent version is byte-for-byte untouched. The repair (now a testable helper) recovers a leading or doubled comma (the empty-system_info artifact) in addition to the existing empty-value / trailing-comma fixes. No agent version bump; self-upgrade and daemon mode are unaffected. Healthy agents of every version behave identically. Full backend suite green. Addresses #31. |
||
|
|
a1192e602d |
feat: HA / VIP (Keepalived) management from the UI (#27)
Manage highly-available virtual IPs backed by Keepalived (VRRP) directly from the OpenManager
UI — no more SSHing into nodes to install/configure Keepalived by hand. Builds on the agent
pull-architecture: define the VIP centrally, click Apply, and the agents converge.
Highlights:
- New "HA / VIP" tab: create a virtual IP, pick a per-node interface, select which pool nodes
participate (MASTER/BACKUP roles + priorities); live MASTER/BACKUP/FAULT per node.
- On Apply, agents install & configure Keepalived (unicast VRRP, cloud-safe default) across the
major distros (Debian/Ubuntu, RHEL/CentOS/Alma/Rocky, Fedora, SUSE/openSUSE, Alpine) with a
HAProxy health-check, so the VIP fails over automatically when HAProxy drops.
- Single-node (a managed floating IP without failover) and multi-node VRRP failover both work.
- VIP changes ride the standard Apply Management flow with the standard "View Change" diff.
- Approval-gated deletion (safety): deleting a running VIP is staged for approval and the VIP
keeps running, untouched, until you approve it — an agent never tears a VIP down without an
explicit human approval. Per-VIP Diagnostics view; opt-in package uninstall (only on nodes
where OpenManager installed it). A node already running a hand-managed Keepalived is detected
and never overwritten ("externally managed").
- Fully opt-in and backward compatible: nodes/clusters without a VIP are unaffected. Adds
vip_instances + vip_members tables (idempotent SCHEMA_VERSION bump; existing data and
passwords unaffected) and a `vip` RBAC permission group.
- Also includes a HAProxy config-generator robustness fix: auto-inject a stick-table when a
frontend uses a stick counter (track-sc / sc_*_rate) but declares none.
On-prem / L2 (VRRP) scope; the UI notes the cloud caveat.
|
||
|
|
f3d4fb11bb |
fix(agent): accept agent token (X-API-Key) on cluster read endpoints (Issue #22)
Assigning a HAProxy agent failed with '401: Authorization header missing'
on GET /api/clusters. A cluster-read hardening had made GET /api/clusters and
GET /api/clusters/{id} accept only a user JWT in the Authorization header;
agents authenticate with their agent token in the X-API-Key header, so the
token was never read.
Both endpoints now accept either a user JWT (Authorization) or an agent token
(X-API-Key via validate_agent_api_key), mirroring the existing dual-auth on
POST /api/agents/generate-install-script. Anonymous access is still rejected,
so the original hardening is preserved. The auth guard is placed before the
try block so the failure surfaces as a clean 401 (not the 500-wrapped-401 in
the report). Agent install scripts now consistently send the token via
X-API-Key (pre-flight cluster check on linux/macos, and macOS get_cluster_paths
which previously used the wrong Authorization: Bearer header).
Also normalizes the platform in the uninstall-script generator so macOS agents
(which report platform 'darwin') no longer get a 400 from
GET /api/agents/generate-uninstall-script/darwin.
version 1.6.0 -> 1.6.2.
|
||
|
|
02b1cb2bca |
feat: v1.5.0 — Site Wizard (Issue #14) + ACME Diagnostic Panel (Issue #13)
Closes #13, Closes #14. This release squashes the v1.4.0 → v1.5.0 development line. v1.4.0 shipped the ACME stability & enterprise audit (Issues #10/#11/#12). v1.5.0 builds on that foundation with two co-equal headline features plus a 22-round audit campaign hardening the prior configuration surface. License remains MIT for v1.5.0 (relicense to AGPL-3.0 lands in v1.5.2). ------------------------------------------------------------------ HEADLINE FEATURE A — ACME Diagnostic Panel (Issue #13) ------------------------------------------------------------------ A live pre-flight + post-failure diagnostic surface for every ACME order, reachable from the ACME Automation page. The panel exists to make ACME failures legible to operators who do NOT have shell access to the API host. Endpoints (`backend/routers/acme_diagnostics.py`): POST /api/letsencrypt/orders/{order_id}/diagnostics Run the full 5-check suite (DNS / port-80 / routing / account / agents) and humanize the order's `error_detail` (>=11 RFC-8555 problem types, backwards compatible with legacy plain-string failures). POST /api/letsencrypt/orders/{order_id}/diagnostics/ {check_id}/rerun Re-run a single check in place — used by the "Re-run" button on every row of the modal's pre-flight table. GET /api/letsencrypt/orders/{order_id}/events Merged event timeline combining the typed `acme_order_events` rows with correlated `user_activity_logs` entries (resource_type = 'letsencrypt_order' AND resource_id = order_id). The diagnostic modal auto-tails this timeline every 5 seconds while open. Service-level checks (`backend/services/acme_diagnostics.py`): * DNS resolution via stdlib socket.gethostbyname_ex through run_in_executor (intentionally avoiding an aiodns runtime dep for v1.5.0). * Port-80 HEAD probe, target locked to the order's domains, success on HTTP 200 OR 404, warns on egress timeout (corp egress policies routinely blackhole outbound 80 — fail-hard would be too noisy). * SSRF guard: probe refuses non-public IPs and surfaces the skip in the diagnostic result; IPv4-mapped IPv6 normalisation closes the `::ffff:169.254.169.254` cloud-metadata vector. * HAProxy routing presence check: matches the order's cluster_ids to a port-80 HTTP frontend. * ACME account validity check against `letsencrypt_accounts`. * Agent presence check (>=1 active agent in target cluster). * Every sub-check wrapped in a wall-clock timeout to bound impact on the API event loop. RBAC: ssl.read for run, ssl.read for events. Per-user 5/min rate limit on both run and rerun, backed by the (user_id, action, created_at DESC) composite index. Frontend (`frontend/src/components/ACMEAutomation.js`): * "Diagnose" button on every order row + the existing "stuck order" warning row. * Modal with two tabs: - Pre-flight Checks (Antd Table with status pills + Re-run buttons + humanized error banner) - Event Log (Antd Timeline with auto-tail polling, scroll- to-bottom, pause-on-hover) * Correlation IDs surfaced in error banners and individual check fail details for backend-log lookup. ------------------------------------------------------------------ HEADLINE FEATURE B — Site Setup Wizard (Issue #14) ------------------------------------------------------------------ A single guided flow that creates a Backend + Servers + HTTP Frontend (and optional HTTPS Frontend) in one atomic transaction. Endpoints (`backend/routers/site_wizard.py`): POST /api/site-wizard/preview — diff-preview the changeset POST /api/site-wizard/create — atomic execute POST /api/site-wizard/reject — clean rollback (including any wizard_staged ACME orders) GET /api/site-wizard/drafts — draft persistence PUT /api/site-wizard/drafts/{id} — save/update DELETE /api/site-wizard/drafts/{id} Feature surface: * One screen captures both backend (mode + servers) AND frontend (http + optional https + SSL mode) inputs. * SSL modes: ACME (new order, HTTP-01 only for v1.5.0), Upload (existing PEM), Existing (link to a stored cert), or None. * ACME-staged path: wizard_staged_until watermark on the `letsencrypt_orders` row defers finalisation until agent confirmation; per-mode reject cleanly cancels and rolls back the staged order. * Live diff preview against the cluster's current generated config (renderer-evolution noise stripped — track-sc<N> dedup, per-server cookie strip, defaults-cookie inheritance, listen-block flattening). * Draft persistence with PEM stripped at save time (private keys never round-trip through the drafts table). * Per-cluster multi-tenancy: drafts and wizard_staged orders are isolated to the creating user's cluster scope. Frontend (`frontend/src/components/SiteWizard.js`): * 4-step Antd Steps flow: Backend → Frontend → SSL → Review. * Render the live diff preview inline before commit. * Antd Form-level validation mirrors backend Pydantic validators (numeric bounds, HAProxy reserved keywords, ALPN consistency, IPv6 scope-id, domain regex, server name dedup). ------------------------------------------------------------------ AUDIT CAMPAIGN — Rounds 1 → 22 (Bulgu #1 → #82) ------------------------------------------------------------------ v1.5.0 includes 22 adversarial review passes. Each round produced its own commit set in the corporate development line; this squash collapses those into the v1.5.0 release artefact. Highlights: Round 1-4 Site Wizard core: dry-run parity, single-line value injection guard, ACL -f pattern-file block, SSL parity, timeout regex, form-state pin. Round 5-7 defaults-cookie inheritance, server-named-cookie guard, fe/be mode mismatch, duplicate server names, health_check_uri + server_address validators. Round 8-10 cookie_name / cookie_options newline-injection guard, dry-run parity (round 9), TCP-mode HTTP-only feature blockers. Round 11 SSL name path traversal + health-check >= 1. Round 12-13 SSL & ACME deep dive (Bulgu #23-#32). Round 14 single-line value injection (Bulgu #33). Round 15-17 ACME multi-tenant UX, numeric bounds, HAProxy reserved keywords, ALPN/TLS consistency, all-backup, multi-domain & multi-user enterprise edges, drain/HSTS/post-completion (Bulgu #34-#53). Round 18-21 concurrency, agent state, TCP-mode HTTP-only, list size caps, IPv6 scope-id, preview account validation, TCP backend + balance uri reject (Bulgu #54-#61). Round 22 FE error visibility + 3x stale-data lockouts, referential integrity + cascade safety, authentication & authorization, multi-cluster isolation, apply_pending_changes concurrency, script injection + bulk import multi-tenancy, prefix-stripped signature comparison (Bulgu #62-#82). ------------------------------------------------------------------ NO CORPORATE-SPECIFIC ARTIFACTS ------------------------------------------------------------------ This squash deliberately sanitises corporate hostnames, container registry references, and TLS secret names into generic placeholders (`your-registry.example.com/your-org`, `haproxy-openmanager*.example.com`, `wildcard-tls`, `taylanbakircioglu/haproxy-openmanager-*`) so the public artefact contains no internal infrastructure detail. Pilot / development history that retained those values stays in the corporate fork and is NOT part of this commit. |
||
|
|
07942a82e8 |
feat: ACME stability & enterprise audit (v1.4.0) — fixes #10 #11 #12
Issue #10 — Silent timezone failure on ACME certificate save - Normalize tz-aware expiry_date to UTC tz-naive before INSERT/UPDATE in ssl_certificates (TIMESTAMP WITHOUT TIME ZONE) — restores ACME download path Issue #11 — Duplicate _acme_challenge_backend in generated config - Generator guard: skip auto-append when backend already rendered - Agent-sync filter: _should_sync_backend() drops system-managed backends and detaches their server rows to prevent orphans - Restore filter: IGNORED_BACKENDS skips reserved names during cluster restore - Parser warning: reserved_backend_names blocks accidental manual import - Cleanup migration: removes orphan rows + cascading server entries (idempotent) Issue #12 — Validated orders required manual completion - New 60s background task complete_pending_acme_orders, flag-independent, multi-replica safe via FOR UPDATE SKIP LOCKED + 30s updated_at watermark - Per-order pg_advisory_lock(0x41434D45, order_id) serializes UI-Complete and auto-task races; idempotency guard returns existing certificate cleanly - retry_order endpoint reports in_progress: true within 30s window so the UI surfaces an info toast instead of duplicating CA requests - ACMEAutomation surfaces stuck orders (status=valid && !ssl_certificate_id) with a one-click Complete action and Cancel fallback; conditional 30s polling Other hardening - ACME state machine error_detail persisted as structured JSON across challenge, finalize, download stages for actionable post-mortems - CertificateRequest Pydantic model: domain regex + min_length/max_length and cluster_ids defaulting to all ACME-enabled clusters when "global" is selected - Renewal cluster fallback now requires acme_enabled=TRUE in addition to active - _complete_certificate preserves manual cluster assignments on renewal, surfaces cluster_errors, raises explicit error on missing private key - Audit logging covers acme_certificate_requested/revoked, ca_chain_imported, account created/deactivated/purged, order retried/cancelled - Settings UI exposes acme.staging_url_override for private test CAs (Pebble) - Schema additions: acme_challenges.attempts (default 0) and last_attempt_at, index idx_letsencrypt_orders_status_updated; all migrations idempotent Tests (41/41 passing) - test_acme_expiry_normalize, test_acme_duplicate_backend, test_acme_state_machine, test_acme_pydantic_validation, test_acme_audit_logging, test_acme_concurrency CI / packaging - docker-build.yml reads version.json and pushes additional product-version tag (e.g. 1.4.0) alongside latest and timestamp build id Closes #10 Closes #11 Closes #12 |
||
|
|
cd4a94beb1 |
feat: agent IP/VIP live update + script update detection banner
- Agent scripts now detect and send ip_address in DAEMON heartbeat (Linux: ip route, macOS: ifconfig) - Backend validates agent-reported IPs via ipaddress stdlib, COALESCE preserves existing on NULL - IP/VIP change logging (non-critical, try/except wrapped) for operational visibility - New source_file_hash column on agent_script_templates for reliable update detection - Migration changed to ON CONFLICT DO NOTHING to prevent overwriting UI-customized scripts on restart - GET /versions returns script_update_available flag (disk hash vs DB hash comparison with fallback) - Frontend Alert banner warns users of new agent script versions and directs to Reset to Defaults - Reset to Defaults and Popconfirm modals explicitly warn about custom script edit loss - Full backward compatibility: old agents without ip_address field continue working unchanged Made-with: Cursor |
||
|
|
5b59accd51 |
fix: resolve asyncpg AmbiguousParameterError on keepalive params
asyncpg cannot infer the PostgreSQL type for $20/$21 when the Python value is None in CASE WHEN $20 IS NOT NULL expressions. Add explicit ::text cast so asyncpg can prepare the statement regardless of whether the agent sends keepalive data or not. This was causing ALL heartbeats to fail with 500 error, making all agents appear offline. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
4082152312 |
fix: resolve stale keepalive state when keepalived stops
- Fix DB: COALESCE(NULLIF) prevented clearing stale MASTER/BACKUP state.
Use CASE WHEN IS NOT NULL to distinguish absent fields (old agents)
from empty fields (keepalived stopped) and properly clear to NULL.
- Fix Redis: explicitly delete cache key when keepalived stops instead
of relying on 90s TTL expiry, preventing stale dashboard data.
- Fix agent scripts: SKIP_TO_DAEMON send_heartbeat now always sends
keepalive_state/keepalive_ip fields (even empty) so backend can
detect stopped keepalived and clear stale data.
- Fix legacy heartbeat endpoint: POST /{agent_id}/heartbeat now also
updates keepalive_state and keepalive_ip fields.
- UI: add horizontal scroll to ClusterManagement table to prevent
column overflow with new Keepalive column.
- UI: add VIP tooltip to Dashboard AgentStatusCard keepalive tag.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
b0feca3c2e |
feat: add keepalived VRRP state (MASTER/BACKUP) detection to agent heartbeat
- Add keepalive_state and keepalive_ip columns to agents table (migration + schema) - Add keepalive fields to AgentHeartbeat Pydantic model (backward compatible) - Update heartbeat endpoint to persist keepalive data to DB and cache in Redis - Add multi-method keepalived detection in agent scripts (journalctl, log files, VIP check) - Update dashboard-stats agents/status API with Redis-first keepalive lookup - Update GET /api/agents to include keepalive_state and keepalive_ip - Show MASTER/BACKUP tag in Dashboard AgentStatusCard - Show keepalive info in Agent Management registered agents table - Add Keepalive column to Cluster Management table with VIP search support Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
995d87df43 |
fix: defaults extraction captures only first defaults section
CRITICAL BUG FIX: - Previous awk pattern allowed multiple defaults sections to be captured - Pattern `!/^defaults/` meant "don't exit if line IS defaults" - This caused duplicate defaults when config had listen before defaults New pattern uses `started` flag: - First `defaults` line: set started=1, begin capturing - Second `defaults` line: started is set, EXIT immediately - Any `frontend/backend/listen`: started is set, EXIT Also includes: debug logging for validation error storage with fallback Tested scenarios: - Normal config (defaults → listen → frontend): ✓ - Listen before defaults: ✓ - Two defaults sections: ✓ Only first captured - No defaults section: ✓ Empty output - Defaults at end of file: ✓ - Empty defaults section: ✓ |
||
|
|
83204c86a8 |
fix: critical security and UX improvements for config management
SECURITY FIX (agent.py): - Ensure ONLY APPLIED versions are sent to agents - Fixed fallback query that could return PENDING versions - Agents will never receive unapproved configurations UX FIX (cluster.py): - Clear validation_error when creating new version via Apply - Prevents stale validation errors from showing after re-apply UX FIX (agent.py): - Clear validation_error when agent successfully applies config - Clear last_validation_error on agent record after success |
||
|
|
e75bfdf706 |
feat: dynamic HAProxy binary path from cluster configuration
- /api/agents/{name}/config now returns haproxy_bin_path, haproxy_config_path, stats_socket_path from cluster
- Agent daemon uses these dynamic paths from API response for validation
- Fallback to local config file if API values not present
- Allows cluster admin to change paths without reinstalling agents
This fixes validation when HAProxy binary is in non-standard location
|
||
|
|
5947caa46d |
fix: move uninstall scripts to backend/utils/agent_scripts for deployment
- Copy uninstall-agent-linux.sh and uninstall-agent-macos.sh to backend/utils/agent_scripts/ (same location as install scripts) - Update generate-uninstall-script endpoint to use the same path pattern as working install script endpoint - Add container-specific fallback paths for robustness - Add debug logging to track which path is used Fixes 404 error when fetching uninstall script in production where /utils/ directory at project root is not deployed. |
||
|
|
0c1d68eb01 |
feat: add uninstall script UI with modern design
- Add new API endpoint to serve uninstall scripts by platform - Display uninstall script alongside install script in setup wizard - Add dedicated delete agent modal with 2-step workflow - Modern UI with gradient banners, platform icons, and info cards - Enhanced uninstall scripts to clean all agent temp/backup files - HAProxy service and config remain untouched during uninstall |
||
|
|
851377aedf |
feat: Add HAProxy proxy name collision prevention system
- Add preserved_listen_blocks column to agents table for storing agent's local listen block names - Implement reserved names check (stats, monitoring, admin, etc.) for frontend/backend creation - Add dynamic collision detection against agent's preserved listen blocks - Apply collision checks to CREATE, UPDATE endpoints and bulk import - Add debug mode for failed config validation (saves to /tmp/haproxy-failed-*.cfg) - Fix JSON character stripping for ACL and use_backend rules - Remove collision protection from agent scripts (now handled by backend) - All collision checks wrapped in try-except for backwards compatibility |
||
|
|
bc8563d455 |
fix: Update agent token association on config change and improve Security UI
- Fix token-agent relationship not updating when agent config changes - Agent's api_key now syncs with DB on heartbeat when using different token - Change Security page badge color from red to blue for better UX |
||
|
|
415439cf17 |
fix: Move pool_id auto-heal logic before server_statuses check
- Previously auto-heal only ran when agent sent server_statuses (which is empty on first heartbeat) - Now auto-heal runs on EVERY heartbeat, ensuring pool_id is fixed immediately - This fixes agents being stuck with NULL pool_id when haproxy config is empty - Agents will now appear in UI correctly even before applying first config |
||
|
|
ba09eb70b9 |
fix: Include pool_id from cluster_id in placeholder-token agent registration path
- Modified auto-registration when using placeholder token to query pool_id from cluster_id in heartbeat - Previously pool_id was copied from placeholder agent (which was NULL) - Now correctly fetches pool_id from haproxy_clusters table using cluster_id - Added extensive debug logging for troubleshooting - Fixes issue where agents were registered with NULL pool_id and not appearing in UI |
||
|
|
761e7fae55 |
debug(agent): add extensive logging for pool_id auto-register troubleshooting
Added debug logs to track: - cluster_id presence in heartbeat payload - Auto-register path execution - Database query results for pool_id lookup - Missing cluster_id warnings This will help identify why pool_id remains NULL during registration. |
||
|
|
8418025744 |
fix(agent): auto-register agents with pool_id from cluster_id
PROBLEM: - Agents registered with pool_id = NULL - UI doesn't show agents without pool assignment - Auto-healing only works when server_statuses present - New agents with empty HAProxy config never get pool_id ROOT CAUSE: - Agent auto-register INSERT excluded pool_id column - Auto-healing (line 1417-1422) only triggers inside 'if server_statuses:' block - Fresh agents send no server_statuses → auto-healing never runs - Agent sits with pool_id = NULL indefinitely ANALYSIS: - Previous commit 1f9d8c2 added auto-healing for edge case (cluster before pool) - But auto-healing has conditional prerequisite: server_statuses must exist - New agents: empty HAProxy config → no server_statuses → no healing - This was working before because agents had config from start SOLUTION: - Extract pool_id from cluster_id during registration - Query haproxy_clusters for pool_id using heartbeat's cluster_id - Insert agent with pool_id immediately (no wait for auto-heal) - Two auto-register paths fixed: with API key + without API key EDGE CASES HANDLED: 1. cluster_id missing → pool_id = NULL → auto-healing fallback ✅ 2. cluster deleted → pool_id = NULL → auto-healing fallback ✅ 3. cluster pool_id NULL → pool_id = NULL → auto-healing works ✅ 4. Old agent script → no cluster_id → auto-healing works ✅ COMPATIBILITY: - Auto-healing preserved (line 1417-1422 untouched) - Two mechanisms work together harmoniously - Backward compatible with existing agents - No breaking changes IMPACT: - New agents visible in UI immediately - Faster registration (no wait for auto-heal) - Reduced heartbeat cycles to full functionality - Better user experience TESTING REQUIRED: 1. Delete agent from database 2. Install fresh agent with cluster_id in config 3. Verify pool_id populated on registration 4. Verify agent appears in UI immediately 5. Verify existing agents unaffected |
||
|
|
a739dd0f95 |
PRODUCTION FIX v2: Direct JSON Sanitization in Heartbeat Endpoint
CRITICAL ISSUE:
Previous middleware approach failed - Starlette middleware cannot
reliably modify request body after it's consumed by FastAPI.
NEW APPROACH - ENDPOINT-LEVEL SANITIZATION:
Moved JSON sanitization directly into agent heartbeat endpoint for
guaranteed execution before Pydantic validation.
ROOT CAUSE CONFIRMED:
Agent daemon mode sends: "server_statuses": ,
This is INVALID JSON (empty value before comma)
Result: JSON decode error at position 322 -> agents stuck offline
SOLUTION IMPLEMENTATION:
Modified: backend/routers/agent.py
- Read raw request body BEFORE Pydantic processing
- Apply 3 regex fixes:
1. "field": , -> "field": null,
2. "field": } -> "field": null}
3. {field,} -> {field}
- Parse sanitized JSON manually
- Create AgentHeartbeat from clean dict
- Continue with normal heartbeat flow
BENEFITS:
✓ NO AGENT SCRIPT CHANGES (production safe)
✓ Guaranteed execution (not middleware dependent)
✓ Detailed logging of sanitization
✓ Graceful error handling
✓ Backward compatible with all agents
✓ Zero impact on valid JSON
LOGGING:
INFO: "Sanitized malformed JSON from agent 'demo-agent1'"
DEBUG: Shows before/after JSON (first 300 chars)
PRODUCTION IMPACT:
- demo-agent1 & agent3 will go online immediately
- No agent restart required
- No agent script update required
- Self-healing for future similar issues
TESTING:
Deploy backend -> Watch logs for:
"Sanitized malformed JSON from agent"
Removed:
- backend/middleware/json_sanitizer.py (approach failed)
This direct approach guarantees the fix executes BEFORE
FastAPI/Pydantic validation, solving the agent offline issue.
|
||
|
|
a6b223c0e7 |
CRITICAL FIX: Enhanced Error Handling for Agent Heartbeat JSON Parse Errors
PRODUCTION STABILITY FIX - Detailed Logging for Malformed Agent Payloads PROBLEM: - demo-agent1 and demo-agent2 sending malformed JSON - Error: JSON decode error at body position 322 - No visibility into WHAT is malformed or WHY - Impossible to debug without raw payload inspection ROOT CAUSE: - FastAPI consumes request body before error handler - Pydantic validation fails but does not show raw input - Agent script might be generating invalid JSON - No logging of actual problematic payload SOLUTION - ENHANCED ERROR HANDLING: 1. MIDDLEWARE ENHANCEMENT (error_handler.py): - Extract RAW body in validation error handler - Parse agent name from JSON (even if malformed) - Log first 500 chars of problematic payload - Add body size to error details - Special handling for /heartbeat endpoint - Detailed logging for json_invalid errors 2. HEARTBEAT ENDPOINT (agent.py): - Added Request parameter for raw body access - Enhanced docstring with troubleshooting info BENEFITS: - Instant visibility into malformed JSON - Agent name logged even on parse failure - Exact payload position + preview - No performance impact (only on errors) - Backward compatible (does not change API) NEXT STEPS (After Deploy): 1. Check logs for CRITICAL JSON PARSE ERROR 2. Identify exact field causing parse failure 3. Fix agent script if needed 4. Or fix backend to be more tolerant This enables root cause analysis without SSH access to agent servers |
||
|
|
550995ec5b |
refactor: Remove unused pending_config_request flag from heartbeat response
CLEANUP: Remove pending_config_request logic from heartbeat BACKGROUND: This flag was added to heartbeat response to help agents detect pending configuration requests immediately, without waiting for the next polling cycle. REASON FOR REMOVAL: - Agent scripts already check for pending config requests every 30 seconds - Adding this flag to heartbeat response is redundant - Creates unnecessary database queries on every heartbeat - No performance benefit in practice HEARTBEAT RESPONSE SIMPLIFIED: - Removed pending_config_request field - Kept only status, message, and agent_id - Simpler API contract - Reduced database load BENEFITS: - Simpler heartbeat API - Fewer database queries (performance improvement) - Agent polling mechanism already works well - No change in agent behavior (agents don't use this flag) PRODUCTION IMPACT: - No breaking changes - Agents continue working normally - Configuration updates still work via polling - Reduced database load |
||
|
|
e1fecde331 |
fix: Remove misleading upgrade completion heartbeat causing agent restart loop
CRITICAL PRODUCTION BUG: Agent stuck in restart loop after upgrade SYMPTOMS: - Agents continuously restarting every ~30 seconds - Log shows: "Sending upgrade completion heartbeat..." - Log shows: "Agent upgrade completed successfully" - systemd restarts agent immediately after - Agents never reach daemon loop - Configuration updates not received - Entity updates not applied ROOT CAUSE: - Agent script v1.0.10 had misleading "upgrade completion heartbeat" - This heartbeat was sent EVERY time daemon started - After sending, script would exit (expecting systemd restart) - systemd would restart agent → infinite loop - Agent never reached check_agent_upgrade() or check_config_updates() MISLEADING CODE (REMOVED): SOLUTION: - Removed "upgrade completion heartbeat" from daemon startup - Agent sends normal heartbeat in daemon loop (every 30s) - No special "upgrade completion" needed - Agent stays in daemon mode continuously - systemd only restarts on actual failures IMPACT: - Agents no longer restart in loop - Configuration updates work normally - Entity updates applied successfully - Upgrade process works correctly - Production stability restored |
||
|
|
e0fb7180ae |
fix: Agent heartbeat cluster-pool auto-healing + global token support
MAIN BUG FIX: - Agent offline issue resolved (cluster created before pool scenario) - 2-method cluster lookup: pool_id -> cluster_id fallback - Auto-healing: pool_id NULL automatically corrected on first heartbeat SECURITY & VALIDATION: - Removed pool-based security check (token is globally usable) - Pool-cluster validation for new agents (frontend + backend) - Relaxed validation for agent upgrades (fallback pool_id tolerated) AGENT IMPROVEMENTS: - HTTP error logging in agent scripts (curl status code check) - Detailed backend error response logging - Better troubleshooting capabilities PRODUCTION SAFE: - Backward compatible (no breaking changes) - Existing agents unaffected (Method 1 priority) - Agent upgrades work (relaxed validation) - Global token model preserved (cross-pool usage OK) |
||
|
|
281e23ea27 |
feat: Add SSL usage_type (Frontend/Server) with conditional private key requirement
This is a comprehensive update that adds SSL certificate differentiation for frontend (HAProxy bind) and server (backend verification) use cases. FEATURES: - SSL certificates can be marked as 'frontend' or 'server' usage type - Frontend SSL: Private key REQUIRED (for HAProxy bind ssl crt) - Server SSL: Private key OPTIONAL (CA cert only for backend verification) - UI dropdown for usage type selection - Dynamic form validation based on usage type - Filtering: Frontends see only Frontend SSL, Backends see only Server SSL DATABASE: - Added usage_type column to ssl_certificates (default: 'frontend') - Made private_key_content nullable for server SSL support - Migration automatically runs on pod restart BACKEND: - Pydantic v2 compatibility (@field_validator, @model_validator) - SSL router: usage_type filtering support - Agent endpoint: usage_type field included - Improved migration robustness with better error handling - Fixed duplicate ensure_agents_table() function - Fixed JSONB permissions insert with json.dumps() - Fixed ON CONFLICT constraints with explicit checks FRONTEND: - SSL Management: Usage Type dropdown with visual feedback - Frontend Management: Filters only Frontend SSL certificates - Backend Servers: Filters only Server SSL certificates - Dynamic private key validation (required for Frontend, optional for Server) - Improved form UX with color-coded hints AGENT SCRIPTS (Linux & macOS): - Support for Server SSL without private key - Conditional PEM file creation (cert+key vs cert-only) - usage_type awareness in SSL deployment - Backward compatible with existing Frontend SSL certificates DOCKER: - Increased npm timeout for slow networks (300s → 600s) - Increased fetch-retries (5 → 10) - Reduced maxsockets for stability (3 → 1) All changes are backward compatible. Existing SSL certificates default to 'frontend' type and continue working unchanged. Tested with: HAProxy 2.8+, PostgreSQL 15, React 18 |
||
|
|
1133fbe229 |
security: Fix critical RBAC vulnerability in user management
Critical security fixes:
- Add admin-only checks for user CRUD operations
- Add admin-only checks for role CRUD operations
- Add admin-only checks for role assignment operations
- Add permission check for agent script generation
- Fix auth_middleware to include is_admin flag in user context
- Hide user/role management buttons from non-admin users in UI
- Add 'View Only' labels for viewer users
Security improvements:
- Prevent viewer users from creating/editing/deleting users
- Prevent viewer users from creating/editing/deleting roles
- Prevent viewer users from assigning roles to users
- Backend API endpoints now properly check admin status
- Frontend UI now hides admin-only actions from viewers
Public release changes:
- Remove company-specific registry URLs from build-images.sh
- Update registry to generic example: your-registry.example.com
Affected endpoints:
- POST /api/users (create user) - admin only
- PUT /api/users/{id} (update user) - admin only
- DELETE /api/users/{id} (delete user) - admin only
- POST /api/roles (create role) - admin only
- PUT /api/roles/{id} (update role) - admin only
- DELETE /api/roles/{id} (delete role) - admin only
- POST /api/users/{id}/roles (assign roles) - admin only
- POST /api/agents/generate-install-script - permission check
|
||
|
|
6aae0f4309 | Initial commit |