Skip to content

Loxy: self-hosted HAProxy management panel (frontends, backends, TLS, domains) - #1

Merged
2shiraz merged 42 commits into
mainfrom
build/phase-0-1
Oct 14, 2026
Merged

2shiraz merged 42 commits into
mainfrom
build/phase-0-1

Conversation

@2shiraz

@2shiraz 2shiraz commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Loxy is a self-hosted management panel for HAProxy 3.4 LTS. You manage frontends and backends; Loxy renders haproxy.cfg, validates it with haproxy -c, swaps it in atomically, and uses runtime API ops where it can (a reload otherwise).

What's in it

  • Frontends: binds (* or a specific IP, many ports), HTTP or TCP, TLS termination with a list of certificates, HTTP→HTTPS redirect, host/path routing rules to backends, a default backend, maintenance (503 / reject), enable/disable, client timeout, maxconn, forwarded headers.
  • Backends: reusable server pools.
    • Servers: an IP, a host from the inventory, or a label selector.
    • Settings: algorithm, port overrides, weight and backup, health checks (tcp/http/https with path, status codes and body), sticky sessions, PROXY protocol v1/v2, server timeout.
    • Server changes are applied at runtime (add/del server, drain before removal).
  • Inventory and node: hosts with labels and a label-selector preview; node addresses.
  • TLS: uploaded and ACME certificates (lego, HTTP-01 through virt@acme_http01, DNS-01 through libdns), hot-swapped with set/commit ssl cert.
  • Domains & DNS: domains attach to a frontend. Verification and auto A/AAAA records use the frontend's bind addresses, or the node's addresses when it binds *. A certificate issued from a domain is attached to its frontend if that frontend terminates TLS.
  • Ops: config revisions with diff and rollback, metrics per frontend and per backend (1 s live, 1 min history), live logs, events and audit log, notifications filtered by label, users and roles, API tokens, TOTP, backup and restore, Prometheus endpoint.
  • CLI: loxy frontends ls, loxy backends ls, loxy api …, render --dry-run, backup/restore, admin reset-password.

Direction change (ADR 0005)

The earlier load-balancer model is gone: LB objects, plans and sizes, rescaling, prices, quotas and delete protection. Everything is built on the frontend/backend model, and provider-specific wording has been removed. The schema was rewritten in place (pre-release). Loxy refuses an old load-balancer database with a clear error instead of failing later.

Decisions (defaults used)

  • Names allow any printable characters, up to 64 (API and UI match).
  • An HTTP frontend may have no default backend (unmatched requests get 503); a TCP frontend requires one.
  • Route matching:
    • Hosts are matched exactly or as *.suffix, case-insensitive, with the port ignored.
    • The path is a prefix match.
    • Rules are evaluated in order, at most 100 per frontend.
  • Health checks:
    • HTTP backends default to HTTP checks.
    • The interval is 3–3600 s, and the timeout must be less than the interval (§8.6).
    • An explicit check port is kept; null means each server's own port.
  • A backend that is in use cannot be deleted (409), and its mode cannot change while it is in use.
  • IPv6: * renders 0.0.0.0:<port> plus ipv6@:::<port> v6only. The IPv6 listener is added only when the host supports IPv6, because HAProxy exits at startup otherwise (verified). LOXY_IPV6=auto|on|off controls this; see ADR 0004 §9.
  • Alert label filters scope frontend, backend and domain events. Node-wide events (apply, config, backup, node) always pass the filter.

Review

Two review passes (correctness, security, performance). Fixed:

  • IPv6 wildcard listeners.
  • :80 listener collisions:
    • when a frontend is enabled via PATCH;
    • when a frontend gets a managed HTTP-01 certificate or a domain;
    • collisions are now family-aware (*, 0.0.0.0, ::);
    • collision checks are serialized with their commit.
  • secure on sticky cookies when every frontend using the backend has TLS.
  • Duplicate certificate ids return 422 instead of 500.
  • Explicit health-check port is preserved.
  • HTTP check default and interval range now follow the spec.
  • Alert scoping for node-wide events; domain events carry their frontend.
  • Server listing is no longer O(servers × slots).
  • One shared rule for when a frontend needs an ACME :80 listener.

Left as is:

  • PUT stays a full replacement: an omitted health check takes the mode default, per the spec.
  • No migration for health-check intervals of 1–2 s: those rows only exist in pre-release dev databases, which are refused anyway.

Verification

  • golangci-lint (0 issues), go vet, go test ./... — all green, with new regression tests for each fix.
  • make test-integration against haproxy:3.4.6-alpine passes, including Pebble ACME.
    • Under load, 20 reloads plus a runtime server add and remove gave 0 failed requests.
  • Playwright e2e against a live stack: 2/2 passed. It covers backend + server → healthy, frontend → 200, maintenance → 503 → 200, typed-confirm delete, and domains on a frontend.
  • npm run check: 0 errors and 0 warnings. The initial bundle is about 55 KB gzip (budget 150 KB).

Not done yet

  • Multi-node (panel/agent) and keepalived VIP (Phase 3).
  • Config import from an existing haproxy.cfg (backlog).
  • The design-skill pass (critique → polish → quieter → harden) screen by screen.

Screenshots: docs/screenshots/.

🤖 Generated with Claude Code

https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc

claude added 22 commits October 14, 2026 00:11
…add design skills

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…golden + haproxy -c test; deploy/CI scaffolding; OpenAPI contract

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…egration suite with all VERIFY items passing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…o v5), DNS providers via libdns, REST API, embedded SPA, loxy CLI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…d UI screenshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…e add/remove, 0 failures); lint fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…s (webhook/Slack/Telegram/SMTP), controller /metrics, nightly backups

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…s validate; ACME waits for applied config; Pebble HTTP-01 integration test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…ric rings (svc 15 min), server rows every 2 s, soft memory limit, opt-in pprof

Measured with 50 LBs / 200 services / 1000 targets: RSS ~48 MB, list p95 6.9 ms, single-resource p95 ~1.2 ms, idle CPU 1.3-3%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…reset-at-connect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…zero-downtime results

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
… HAProxy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
@2shiraz 2shiraz changed the title Loxy Phase 0 + Phase 1 MVP (incl. Domains & DNS) Loxy: Phase 0 + Phase 1 MVP + most of Phase 2 (incl. Domains & DNS) Oct 14, 2026
…zard and service editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…le/disable and login enforcement

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
… stream), LOXY_LIVE_LOGS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
claude added 10 commits October 14, 2026 01:41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
… HAProxy, bounded metrics queries, dual-stack IP assignment + race, reject IPv6 zones (config injection), admin-only reads of users/alert rules, owner/2FA/session hardening, cert quota on domain issuance, nil guards, ordered snapshot refresh, rollback re-pushes ACME tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…y after runtime-then-failed-reload

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Rewrite the spec, README and ADRs around frontends and backends instead of
load-balancer objects, plans, rescaling and prices. Add ADR 0005 recording
the direction change and retire the cross-frontend cap note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…iler

Replace load balancers, services, targets and plans with reusable backends
(server pools, health checks, sticky sessions, PROXY protocol) and frontends
(binds, TLS, redirect, host/path routes to backends). Domains attach to a
frontend; DNS verification and auto records use its bind addresses.
CLI gains `frontends ls` / `backends ls`. Integration tests rebuilt on the
new model; drop an accidentally committed test binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
Rebuild the UI for the frontend/backend model, remove the load-balancer
wizard, rescale and networking pages, and fix date helpers to read the
API's millisecond timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
@2shiraz 2shiraz changed the title Loxy: Phase 0 + Phase 1 MVP + most of Phase 2 (incl. Domains & DNS) Loxy: self-hosted HAProxy management panel (frontends, backends, TLS, domains) Oct 14, 2026
The shared default transport keeps 2 idle conns per host, so 6 of the 8
load workers dialled per request and exhausted ephemeral ports on the CI
runner, failing the zero-downtime test and the next test's dial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
…icky cookie

- "*" binds render 0.0.0.0 plus ipv6@:::port v6only when the host has IPv6
  (startup probe, LOXY_IPV6=auto|on|off); HAProxy exits otherwise.
- Listener collisions also checked when a frontend is enabled via PATCH,
  gains a managed HTTP-01 cert, or gets a domain (implicit :80 ACME).
- Sticky cookie gets secure when every frontend using the backend has TLS.
- Duplicate certificate ids are a field error instead of a 500.
- Explicit health-check port is kept; http backends default to http
  checks; interval 3-3600 (spec 8.6); UI validation matches.
- Node-wide events (apply/backup) are not narrowed by alert label filters.
- Server views compute backend health once per request.
- Refuse pre-release load-balancer databases with a clear error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
… collisions

- Node-wide event kinds (apply/config/backup/node) bypass alert label filters
  explicitly; domain events carry their frontend id.
- Listener-changing requests are serialized so a collision check and its
  commit cannot interleave; domain DNS work stays outside the lock.
- Collisions are family-aware (* both, 0.0.0.0 IPv4, :: IPv6); explicit ::
  binds are v6only.
- One NeedsACME rule shared by the renderer and the API checks.
- Legacy-schema refusal happens before anything is written.
- LOXY_IPV6 also accepts booleans; secure-cookie map computed once per render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159fEExV6P6AdgP7HbsJNBc
@2shiraz
2shiraz marked this pull request as ready for review October 14, 2026 08:28
@2shiraz
2shiraz merged commit 97cb785 into main Oct 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants