mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-02 20:29:43 +00:00
Make getting-started installs fail closed and plans current
Chain signed installer downloads, verification and execution in the English, German and Spanish landing pages so a failed trust step cannot run stale or unverified bytes. Keep the pinned signer, namespace and release URLs, preserve real installer exits, and correct the existing plan and paired-app retirement guidance. Exercise the copyable Bash gate with offline command stubs and run those documentation regressions in Public docs CI. Contract-Neutral: Public documentation and its secret-free CI validation only; no runtime, entitlement, release-selection or subsystem contract delta. Change-source: pulse-maintainer
This commit is contained in:
parent
50c24260ca
commit
439604c0bd
5 changed files with 241 additions and 24 deletions
5
.github/workflows/public-docs.yml
vendored
5
.github/workflows/public-docs.yml
vendored
|
|
@ -9,6 +9,7 @@ on:
|
|||
- ".github/ISSUE_TEMPLATE/**"
|
||||
- "scripts/check_public_docs.py"
|
||||
- "scripts/check_docs_mirror.py"
|
||||
- "scripts/readme_entrypoints_test.py"
|
||||
- ".github/workflows/public-docs.yml"
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -20,6 +21,7 @@ on:
|
|||
- ".github/ISSUE_TEMPLATE/**"
|
||||
- "scripts/check_public_docs.py"
|
||||
- "scripts/check_docs_mirror.py"
|
||||
- "scripts/readme_entrypoints_test.py"
|
||||
- ".github/workflows/public-docs.yml"
|
||||
|
||||
permissions:
|
||||
|
|
@ -42,5 +44,8 @@ jobs:
|
|||
- name: Validate public documentation
|
||||
run: python3 scripts/check_public_docs.py
|
||||
|
||||
- name: Check getting-started safety and current plans
|
||||
run: python3 scripts/readme_entrypoints_test.py
|
||||
|
||||
- name: Check shipped docs mirror sync
|
||||
run: python3 scripts/check_docs_mirror.py
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -91,17 +91,19 @@ The installer is signed. Verify `install.sh` against the pinned
|
|||
|
||||
```bash
|
||||
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"
|
||||
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
|
||||
-s install.sh.sshsig < install.sh &&
|
||||
bash install.sh --version "${PULSE_VERSION}"
|
||||
rm -f install.sh install.sh.sshsig
|
||||
```
|
||||
|
||||
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**.
|
||||
|
|
@ -133,15 +135,19 @@ agent can and cannot do at each privilege level.
|
|||
|
||||
- **Community** — self-hosted monitoring, seven days of metric history, core
|
||||
SSO, update alerts, and Patrol with your own provider or local model.
|
||||
- **Relay** — Community plus secure remote web access, Pulse Mobile pairing,
|
||||
push notifications, and fourteen days of history.
|
||||
- **Pro** — Relay plus Patrol investigation, governed fixes, ninety days of
|
||||
- **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](https://pulserelay.pro/msp).
|
||||
|
||||
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](docs/PULSE_PRO.md) and
|
||||
[current plans](https://pulserelay.pro) for details.
|
||||
|
|
|
|||
|
|
@ -10,10 +10,17 @@ absichtlich unverändert.
|
|||
|
||||
Pulse ist ein self-hosted Monitoring-Arbeitsbereich für Proxmox, Docker,
|
||||
Kubernetes, TrueNAS und verwandte Infrastruktur. Community deckt das
|
||||
Kernmonitoring kostenlos ab. Relay ergänzt sicheren Remote-Zugriff auf die
|
||||
Pulse-Weboberfläche, Pulse-Mobile-Handoff-Pairing, Push-Benachrichtigungen und
|
||||
14 Tage Verlauf. Pro ergänzt Ursachenanalyse, sichere Remediation-Workflows,
|
||||
Operations-Werkzeuge, Governance-Funktionen und 90 Tage Verlauf.
|
||||
Kernmonitoring kostenlos ab. Pro ergänzt Ursachenanalyse, sichere
|
||||
Remediation-Workflows, Operations-Werkzeuge, Governance-Funktionen und 90 Tage
|
||||
Verlauf.
|
||||
|
||||
Relay wird nicht mehr verkauft. Bestehende Relay-Abonnenten erhalten
|
||||
Pro-Funktionen zum bisherigen Preis, solange ihr Abonnement weiterläuft.
|
||||
Bereits gekoppelte Telefone funktionieren bis zum **31. März 2027** weiter.
|
||||
Relay verbindet Pulse mit der App, nicht mit der Weboberfläche. Für den
|
||||
Remote-Zugriff auf die Weboberfläche nutze ein eigenes VPN oder einen Tunnel.
|
||||
Für Warnmeldungen auf dem Telefon füge ein Ziel für ntfy, Gotify oder Pushover
|
||||
hinzu und öffne Pulse im Browser deines Telefons.
|
||||
|
||||
## Bezahlte Relay-, Pro- und Legacy-Kunden
|
||||
|
||||
|
|
@ -37,17 +44,20 @@ Installer-Datei und führe den Installer auf deinem Proxmox-Host aus:
|
|||
|
||||
```bash
|
||||
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"
|
||||
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
|
||||
-s install.sh.sshsig < install.sh &&
|
||||
bash install.sh --version "${PULSE_VERSION}"
|
||||
rm -f install.sh install.sh.sshsig
|
||||
```
|
||||
|
||||
Füge den gesamten Block ein: Schlägt ein Download oder die Signaturprüfung fehl,
|
||||
wird der Installer nicht ausgeführt. Führe den letzten Befehl nicht separat aus,
|
||||
um einen Fehler zu umgehen.
|
||||
|
||||
Agent-Installationen und v5-zu-v6-Agent-Upgrades verwenden den Befehl, den
|
||||
Pulse unter **Settings → Infrastructure → Install on a host** erzeugt. Dieser
|
||||
Befehl wird von deinem Pulse-Server über `/install.sh` bereitgestellt.
|
||||
|
|
|
|||
|
|
@ -10,11 +10,16 @@ la interfaz se conservan sin traducir de forma intencional.
|
|||
|
||||
Pulse es un workspace de monitoreo autohospedado para Proxmox, Docker,
|
||||
Kubernetes, TrueNAS e infraestructura relacionada. Community incluye el
|
||||
monitoreo principal gratis. Relay añade acceso remoto seguro a la interfaz web
|
||||
de Pulse, emparejamiento con Pulse Mobile para handoff, notificaciones push e
|
||||
historial de 14 días. Pro añade análisis de causa raíz, flujos de remediación
|
||||
seguros, herramientas operativas, funciones de gobernanza e historial de 90
|
||||
días.
|
||||
monitoreo principal gratis. Pro añade análisis de causa raíz, flujos de
|
||||
remediación seguros, herramientas operativas, funciones de gobernanza e
|
||||
historial de 90 días.
|
||||
|
||||
Relay ya no se vende. Los suscriptores actuales de Relay reciben funciones Pro
|
||||
al precio actual mientras continúe su suscripción. Los teléfonos ya emparejados
|
||||
siguen funcionando hasta el **31 de marzo de 2027**. Relay conecta la app,
|
||||
no la interfaz web. Para acceder a la interfaz web fuera de casa, usa tu propia
|
||||
VPN o un túnel. Para alertas en el teléfono, añade un destino de ntfy, Gotify o
|
||||
Pushover y abre Pulse en el navegador de tu teléfono.
|
||||
|
||||
## Clientes de pago Relay, Pro y legacy
|
||||
|
||||
|
|
@ -37,17 +42,20 @@ host Proxmox:
|
|||
|
||||
```bash
|
||||
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"
|
||||
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
|
||||
-s install.sh.sshsig < install.sh &&
|
||||
bash install.sh --version "${PULSE_VERSION}"
|
||||
rm -f install.sh install.sh.sshsig
|
||||
```
|
||||
|
||||
Pega el bloque completo: si falla una descarga o la verificación de firma, el
|
||||
instalador no se ejecuta. No ejecutes el último comando por separado para omitir
|
||||
un fallo.
|
||||
|
||||
Las instalaciones de agentes y las actualizaciones de agentes de v5 a v6 usan
|
||||
el comando que Pulse genera en **Settings → Infrastructure → Install on a
|
||||
host**. Ese comando se sirve desde tu servidor Pulse en `/install.sh`.
|
||||
|
|
|
|||
188
scripts/readme_entrypoints_test.py
Normal file
188
scripts/readme_entrypoints_test.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Exercise the public landing pages' signed-install gate without network access."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def read_repo_text(relative_path: str) -> str:
|
||||
return (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
ENTRYPOINTS = (
|
||||
"README.md",
|
||||
"docs/i18n/de/README.md",
|
||||
"docs/i18n/es/README.md",
|
||||
)
|
||||
SIGNER = (
|
||||
'pulse-installer namespaces="pulse-install" ssh-ed25519 '
|
||||
"AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
|
||||
)
|
||||
|
||||
|
||||
def installer_commands(text: str) -> str:
|
||||
for block in re.findall(r"```bash\n(.*?)\n```", text, flags=re.DOTALL):
|
||||
if "ssh-keygen -Y verify" not in block:
|
||||
continue
|
||||
lines = block.splitlines()
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("bash install.sh --version "):
|
||||
# Only exercise the download -> verification -> execution gate,
|
||||
# never any subsequent file-cleanup recipe.
|
||||
return "\n".join(lines[: index + 1]) + "\n"
|
||||
raise AssertionError("signed server-installer command block is missing")
|
||||
|
||||
|
||||
STUB = """#!/usr/bin/python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
name = Path(sys.argv[0]).name
|
||||
trace = Path(os.environ['TRACE'])
|
||||
events = [json.loads(line) for line in trace.read_text().splitlines()] if trace.exists() else []
|
||||
event = {'command': name, 'argv': sys.argv[1:]}
|
||||
if name == 'ssh-keygen':
|
||||
event['allowed_signers'] = Path(sys.argv[sys.argv.index('-f') + 1]).read_text()
|
||||
with trace.open('a') as out:
|
||||
out.write(json.dumps(event) + '\\n')
|
||||
if name == 'curl':
|
||||
attempt = 1 + sum(event['command'] == 'curl' for event in events)
|
||||
code = int(os.environ.get('CURL_' + str(attempt), '0'))
|
||||
if code == 0:
|
||||
# Public dummy bytes only. The installer itself is never executed.
|
||||
Path(sys.argv[-1].rsplit('/', 1)[-1]).write_text('downloaded fixture\\n')
|
||||
elif name == 'ssh-keygen':
|
||||
sys.stdin.read()
|
||||
code = int(os.environ.get('VERIFY', '0'))
|
||||
elif name == 'bash':
|
||||
code = int(os.environ.get('INSTALL', '0'))
|
||||
else:
|
||||
raise AssertionError('unexpected stub command')
|
||||
sys.exit(code)
|
||||
"""
|
||||
|
||||
|
||||
def exercise_install(commands: str, exits: dict[str, str]) -> tuple[int, list[dict]]:
|
||||
with tempfile.TemporaryDirectory(prefix="pulse-readme-gate-") as directory:
|
||||
root = Path(directory)
|
||||
stubs = root / "bin"
|
||||
stubs.mkdir()
|
||||
for name in ("curl", "ssh-keygen", "bash"):
|
||||
stub = stubs / name
|
||||
stub.write_text(STUB, encoding="utf-8")
|
||||
stub.chmod(0o700)
|
||||
# A failed download must not execute files left by an earlier attempt.
|
||||
(root / "install.sh").write_text("stale installer fixture\n", encoding="utf-8")
|
||||
(root / "install.sh.sshsig").write_text("stale signature fixture\n", encoding="utf-8")
|
||||
trace = root / "trace.jsonl"
|
||||
completed = subprocess.run(
|
||||
["/bin/bash", "-c", commands],
|
||||
cwd=root,
|
||||
env={"PATH": f"{stubs}:/usr/bin:/bin", "TRACE": str(trace), **exits},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
if completed.stderr:
|
||||
raise AssertionError(completed.stderr)
|
||||
events = [json.loads(line) for line in trace.read_text().splitlines()]
|
||||
return completed.returncode, events
|
||||
|
||||
|
||||
class ReadmeEntrypointsTest(unittest.TestCase):
|
||||
def test_signed_install_stops_at_each_failed_trust_step(self) -> None:
|
||||
cases = (
|
||||
({"CURL_1": "22"}, 22, ["curl"]),
|
||||
({"CURL_2": "23"}, 23, ["curl", "curl"]),
|
||||
({"VERIFY": "1"}, 1, ["curl", "curl", "ssh-keygen"]),
|
||||
)
|
||||
for relative_path in ENTRYPOINTS:
|
||||
commands = installer_commands(read_repo_text(relative_path))
|
||||
for exits, expected_exit, expected_steps in cases:
|
||||
with self.subTest(page=relative_path, exits=exits):
|
||||
code, events = exercise_install(commands, exits)
|
||||
self.assertEqual([event["command"] for event in events], expected_steps)
|
||||
self.assertEqual(code, expected_exit)
|
||||
|
||||
def test_verified_install_keeps_pinned_version_and_signer(self) -> None:
|
||||
for relative_path in ENTRYPOINTS:
|
||||
with self.subTest(page=relative_path):
|
||||
commands = installer_commands(read_repo_text(relative_path))
|
||||
self.assertIn(SIGNER, commands)
|
||||
code, events = exercise_install(commands, {})
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(
|
||||
[event["command"] for event in events],
|
||||
["curl", "curl", "ssh-keygen", "bash"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[event["argv"][-1] for event in events[:2]],
|
||||
[
|
||||
"https://github.com/rcourtman/Pulse/releases/download/vX.Y.Z/install.sh",
|
||||
"https://github.com/rcourtman/Pulse/releases/download/vX.Y.Z/install.sh.sshsig",
|
||||
],
|
||||
)
|
||||
self.assertTrue(all(event["argv"][0] == "-fsSLO" for event in events[:2]))
|
||||
self.assertEqual(events[2]["allowed_signers"], SIGNER + "\n")
|
||||
verify = events[2]["argv"]
|
||||
for option, value in (
|
||||
("-I", "pulse-installer"),
|
||||
("-n", "pulse-install"),
|
||||
("-s", "install.sh.sshsig"),
|
||||
):
|
||||
self.assertEqual(verify[verify.index(option) + 1], value)
|
||||
self.assertEqual(events[3]["argv"], ["install.sh", "--version", "vX.Y.Z"])
|
||||
|
||||
def test_installer_failure_remains_nonzero(self) -> None:
|
||||
for relative_path in ENTRYPOINTS:
|
||||
with self.subTest(page=relative_path):
|
||||
code, events = exercise_install(
|
||||
installer_commands(read_repo_text(relative_path)), {"INSTALL": "17"}
|
||||
)
|
||||
self.assertEqual(code, 17)
|
||||
self.assertEqual(events[-1]["command"], "bash")
|
||||
|
||||
def test_entrypoint_plans_do_not_offer_retired_relay(self) -> None:
|
||||
expected = {
|
||||
"README.md": (
|
||||
"31 March 2027", "Relay is no longer sold", "current price",
|
||||
"for as long as their subscription continues", "own VPN or tunnel",
|
||||
"ntfy, Gotify or Pushover", "not the web UI",
|
||||
),
|
||||
"docs/i18n/de/README.md": (
|
||||
"31. März 2027", "Relay wird nicht mehr verkauft", "bisherigen Preis",
|
||||
"solange ihr Abonnement weiterläuft", "eigenes VPN oder einen Tunnel",
|
||||
"ntfy, Gotify oder Pushover", "nicht mit der Weboberfläche",
|
||||
),
|
||||
"docs/i18n/es/README.md": (
|
||||
"31 de marzo de 2027", "Relay ya no se vende", "precio actual",
|
||||
"mientras continúe su suscripción", "tu propia VPN o un túnel",
|
||||
"ntfy, Gotify o Pushover", "no la interfaz web",
|
||||
),
|
||||
}
|
||||
stale_claims = (
|
||||
"**Relay** — Community plus", "**Pro** — Relay plus",
|
||||
"Relay ergänzt sicheren Remote-Zugriff", "Relay añade acceso remoto seguro",
|
||||
)
|
||||
for relative_path, fragments in expected.items():
|
||||
with self.subTest(page=relative_path):
|
||||
prose = " ".join(read_repo_text(relative_path).split())
|
||||
for fragment in fragments:
|
||||
self.assertIn(fragment, prose)
|
||||
for claim in stale_claims:
|
||||
self.assertNotIn(claim, prose)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue