rcourtman 295edef4d5 Bind SAML logins to the browser that started them (#2729)
## Why

With `allowIdpInitiated` off, the default, every SAML login failed.
`SAMLService.ProcessResponse` passed crewjam/saml an empty list of
request IDs, and crewjam refuses any Response whose `InResponseTo` is
not in that list, so both SP-initiated and unsolicited Responses ended
in "Authentication failed". Only providers with IdP-initiated logins
enabled, which skips the check, could sign in.

## What changes

- `/api/saml/{id}/login` records each AuthnRequest ID, on the provider's
service, with the login's `returnTo` and the hash of a random token it
sets as the `HttpOnly`, `SameSite=Lax` cookie `pulse_saml_login`
(`__Host-pulse_saml_login` over HTTPS). A browser that already holds a
token keeps it, so logins started one after another in several tabs all
stay valid. Records last ten minutes, 1,024 per provider, oldest
evicted.
- The IdP posts its Response cross-site, and browsers withhold
`SameSite=Lax` cookies from cross-site POSTs. The ACS answers a
cookieless POST with a small page that re-posts the same fields once
from Pulse's own origin (no form action, so path prefixes survive; CSP
nonce on the script; a no-JS Continue button; a marker field stops a
second repost).
- `ProcessResponse` reads the answered request ID from the canonical
content goxmldsig verified for the signature covering the assertion
crewjam returns: the signed Response (or the Response in a signed
ArtifactResponse), otherwise that assertion's own
`SubjectConfirmationData`. It spends the record only when it is bound to
the presenting browser, and the login returns to the recorded `returnTo`
rather than the posted `RelayState`. A Response for another browser
leaves that browser's record intact.
- The signature verifier refuses signed content carrying a prefixed
attribute or namespace declaration named like an un-namespaced attribute
crewjam reads. encoding/xml fills such a field from any attribute with
that local name, and exclusive canonicalization leaves an unused
declaration out of the signed form. On the base commit, appending
`xmlns:IssueInstant` and `xmlns:NotOnOrAfter` to a two-hour-old signed
Response got it accepted, so with `allowIdpInitiated` on a captured
login could be replayed indefinitely.
- RelayState is now URL-escaped into the redirect, so a `returnTo` with
its own query string reaches the IdP whole.
- With `allowIdpInitiated` on, behaviour is otherwise unchanged: no
cookie or repost needed, posted `RelayState` honoured.
- The SLO LogoutRequest store from #2713 is generalised into a shared
record store; its `consume` semantics are unchanged.

Stacked on #2714 (on #2713, on #2707). Those three conflict with main in
`storage-recovery.md`, where their boundary notes and main's land at the
same anchor, so none can merge on its own. This branch carries a merge
of main that keeps both sets of notes; landing it lands the stack.

## Verification

- New tests in `internal/api/saml_handlers_more_test.go`,
`saml_service_test.go` and `audit_handlers_test.go`: default-settings
login through the repost, multiple tabs, refusal of another browser's,
unknown, unsolicited and replayed Responses (the refused browser's own
login still completes afterwards), the `xmlns:InResponseTo` spoof with
the Response signed and with only the Assertion signed, a
confirmation-less signed assertion spliced ahead of another browser's
assertion, assertion-only signed success, stale IdP-initiated Responses
with spoofed ages, repost escaping and CSP nonce, and the audit event
for a binding refusal.
`TestContract_SAMLManualIDPCertificateVerifiesIdPSignatures` now runs at
default settings instead of enabling IdP-initiated logins.
- Negative controls by `-overlay`: with the attribute refusal disabled
the stale-Response replay is accepted again; with the refusal disabled
and the binding read from crewjam's parsed fields the
`xmlns:InResponseTo` spoof logs in; the round-one binding code accepted
the spliced-assertion case as the other user.
- A real Chromium run against a test IdP on `127.0.0.1` and the SP
handlers on `localhost`, behind `SecurityHeadersWithConfig` in
production CSP mode: the IdP's POST arrived `Sec-Fetch-Site: cross-site`
without the login cookie, the repost arrived `same-origin` with it, and
the login landed signed in at
`/alerts?range=7d&saml=success&tab=history`.

## Known limits

- Records live in memory: a restart, a provider re-initialization, or a
login started on a host that does not share cookies with the configured
public URL leaves the Response unmatched, and the user signs in again.
- Two logins started at the same moment in a browser that holds no token
yet each mint one; the last cookie wins and the other login fails.
- As with OIDC login state, a sustained flood of unauthenticated login
starts can evict pending records.
- On plain HTTP, or HTTPS that Pulse does not recognise behind a proxy,
the cookie has no `__Host-` prefix, so a sibling subdomain could plant
one, the same exposure the session cookie has there.
2026-10-08 02:45:20 +01:00
2026-10-05 21:53:41 +01:00
2026-03-18 16:06:30 +00:00
2026-10-05 21:53:41 +01:00
2026-03-18 16:06:30 +00:00
2025-10-11 23:29:47 +00:00
2026-03-18 16:06:30 +00:00
2026-10-05 21:53:41 +01:00

Pulse

Pulse logo

Real-time Proxmox, Docker and Kubernetes monitoring that finds what needs attention.

GitHub Stars GitHub Release Docker Pulls License

Live demo · Documentation · Releases · Discussions

Pulse is a self-hosted monitoring workspace for Proxmox, Docker, Kubernetes, TrueNAS, physical and virtual machines, and early-access VMware vSphere environments. It combines live infrastructure state, history, alerts, recovery visibility, and scheduled health checks without requiring a conventional enterprise monitoring stack.

Pulse Proxmox workspace

Why Pulse

  • It watches between visits. Alerts and Pulse Patrol find failed backups, capacity pressure, restart loops, unhealthy containers, clock drift, and other problems that dashboards cannot surface when nobody is looking.
  • It keeps each platform familiar. Proxmox, Docker, Kubernetes, TrueNAS, vSphere, and machines have dedicated views, backed by one shared resource model for search, alerts, history, and investigation.
  • It stays operator-controlled. Credentials are encrypted at rest, API tokens are scoped, agent commands are disabled by default, and governed fixes require the configured policy and approval path.

Platform coverage

Platform Coverage
Proxmox VE, PBS, and PMG Nodes, guests, storage, backups, replication, Ceph, mail gateways, and alerts
Docker and Podman Hosts, containers, Compose projects, Swarm services, health, images, and updates
Kubernetes Clusters, nodes, workloads, pods, services, storage, and events through the unified agent
TrueNAS SCALE and CORE Pools, datasets, disks, snapshots, replication tasks, apps, VMs, and alerts
Linux, Windows, and macOS machines Host health, filesystems, networking, temperatures, RAID, and availability through the unified agent
VMware vSphere Early-access inventory, hosts, clusters, VMs, datastores, networks, snapshots, and recovery context; validate against your own vCenter before production use

Platform pages keep storage and recovery information beside the infrastructure it belongs to. Alerts, Actions, and Patrol remain cross-platform views.

Patrol: monitoring that does rounds

Pulse Patrol runs scheduled checks across the current state and recent history of your infrastructure. Community installations can use a local model or their own AI provider for watch-only analysis. Pulse Pro adds investigation and policy-bound fixes with approval, verification, and an audit trail.

Pulse Patrol attention queue

Pulse also includes an interactive Assistant and an MCP adapter for external clients such as Claude Code and OpenCode. Both sit on top of the same scoped inventory, metrics, alert, storage, and governed-action contracts.

Quick start

Choose an exact version from the latest release and keep that version pinned during installation.

Docker

docker run -d \
  --name pulse \
  -p 7655:7655 \
  -v pulse_data:/data \
  -e PULSE_DEPLOYMENT_METHOD=docker_run \
  --restart unless-stopped \
  rcourtman/pulse:vX.Y.Z

Open http://<your-ip>:7655 and follow the bootstrap-token setup. Docker host monitoring is provided by the unified agent; the Pulse server container does not need the Docker socket.

Proxmox LXC, Linux, and Kubernetes

The installer is signed. Verify install.sh against the pinned pulse-installer key before running it:

export PULSE_VERSION=vX.Y.Z
curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" &&
curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" &&
ssh-keygen -Y verify \
  -f <(printf '%s\n' 'pulse-installer namespaces="pulse-install" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer') \
  -I pulse-installer \
  -n pulse-install \
  -s install.sh.sshsig < install.sh &&
bash install.sh --version "${PULSE_VERSION}"

Paste the whole block: installation stops if either download or signature verification fails. Do not run the final command separately to bypass a failure.

The GitHub installer installs the Pulse server. Install and upgrade agents (including v5-to-v6 agent upgrades) with the per-host command generated under Settings → Infrastructure → Install on a host.

Do you need an agent?

Often not. Proxmox VE, PBS, and PMG are monitored through the Proxmox API with a read-only token, so nothing runs on the host and the generated setup script creates a privilege-separated monitoring user. Install the unified agent only where you want data the platform API cannot provide, such as host SMART health, temperatures, Docker hosts, or standalone machines.

The agent is operator-controlled by design. Command execution is off by default, the local listener binds to localhost, and generated systemd units ship hardened. On Linux, the installer's --least-privilege profile runs the service as a dedicated non-root user, with optional scoped sudo grants for the few collectors that need elevation. The agent security model documents exactly what the agent can and cannot do at each privilege level.

Important

GitHub release assets and rcourtman/pulse images are Community builds. Relay, Pro, and eligible legacy customers should use the private image or Linux archive provided by the Pulse download portal. Replacing a private Pro runtime with a public Community build removes its private runtime hooks.

Editions

  • Community — self-hosted monitoring, seven days of metric history, core SSO, update alerts, and Patrol with your own provider or local model.
  • Pro — Community plus Patrol investigation, governed fixes, ninety days of history, centralized agent profiles, RBAC, audit logging, and reporting.
  • MSP — for managed service providers: one Pulse Account running many client workspaces, each with an isolated Pulse runtime — separate dashboards, alerts, users, audit history, and reports. Free sixty-day two-client evaluation at Pulse for MSPs.

Relay is no longer sold. Existing Relay subscribers receive Pro features at their current price for as long as their subscription continues. Existing paired phones keep working until 31 March 2027. Relay connects the app, not the web UI. Use your own VPN or tunnel for remote web access. For phone alerts, add an ntfy, Gotify or Pushover destination and open Pulse in your phone's browser.

Core self-hosted monitoring is not gated by monitored-system or child-resource volume. See the runtime-aligned capability reference and current plans for details.

Documentation

Localized getting started guides: Deutsch · Español

Development

Pulse uses Go 1.26 and a SolidJS/TypeScript frontend. The managed development runtime starts the frontend at http://127.0.0.1:5173 and proxies API and WebSocket traffic to the backend on port 7655.

npm ci
npm --prefix frontend-modern ci
npm run dev

Useful checks:

go test ./...
npm --prefix frontend-modern test
npm --prefix frontend-modern run type-check
python3 scripts/check_public_docs.py

See CONTRIBUTING.md before investing in a code change. Pulse uses an issue-first contribution process and does not normally accept unsolicited pull requests.

Community and support

License

Everything in this repository is licensed under the MIT License. There is no dual licensing and no commercial or source-available code here.

Pulse Pro is a separate commercial product, built from private sources and distributed through pulserelay.pro. Its use is governed by the Terms of Service, which apply to that product only, not to anything in this repository.

S
Description
Monitoring for Proxmox, Docker, Kubernetes, TrueNAS, and vSphere that watches your infrastructure for you: smart alerts, AI patrols that catch silent failures, and verified fixes
Readme MIT 429 MiB
Languages
Go 61.3%
TypeScript 29%
Python 5.5%
JavaScript 2%
Shell 1.8%
Other 0.3%