mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Integrate reviewed credential-safe MSP diagnostics
Reviewed exact PI candidate 5e6d5af3b5. Existing guidance and shipped mirror now use private header files, verified HTTPS and bounded read-only checks. Retain source-e1x7r92a installer/vet, source-oypceg0j docs tests and Chromium/WebKit rendering evidence; no native tenant isolation or publication claim.
Change-source: pulse-maintainer
This commit is contained in:
+61
-20
@@ -110,11 +110,37 @@ the tunnels and keep the management port out of them.
|
||||
|
||||
### Validation checklist (run after setup, repeat after network changes)
|
||||
|
||||
Use your actual HTTPS addresses and keep certificate verification enabled.
|
||||
For a private CA, add `--cacert /path/to/trusted-ca.pem` using a CA you have
|
||||
verified independently; do not bypass a certificate error with `--insecure`.
|
||||
The examples make bounded **GET** requests, discard response bodies and print
|
||||
only the HTTP status. They do not send notifications or change settings.
|
||||
|
||||
For the authenticated checks, prepare two separate private header files with
|
||||
[the API token file procedure](API.md#api-token-recommended), substituting
|
||||
`agent-header` and `org-a-header` for `api-header`. Keep the directory mode
|
||||
`700` and files mode `600`, owned by the account running curl. Enter the token
|
||||
only in your local editor, never in a shell command, URL or report. Each file
|
||||
contains one `X-API-Token` header: the existing `agent:report` token in
|
||||
`agent-header`, and an existing org-A token with `monitoring:read` in
|
||||
`org-a-header`. Do not broaden scopes or create a privileged token just to run
|
||||
these checks. Keep the files outside repositories, backups shared for support
|
||||
and diagnostic exports; remove them locally when no longer needed.
|
||||
|
||||
Keep `--disable` first so a local curl configuration cannot enable tracing,
|
||||
extra credentials or redirects. Do not add verbose/trace output or follow
|
||||
redirects with these tokens. For the HTTP-status checks below, a TLS, DNS,
|
||||
timeout or connection error is a **failed check**, not proof of isolation:
|
||||
require curl exit **0** as well as the expected HTTP status. `000` is not an HTTP result. Do not use `--fail` for these
|
||||
checks because the expected rejections are HTTP errors.
|
||||
|
||||
1. **Agent port excludes management surfaces.** Both must return `404`:
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' https://agents.example.com:7656/ # 404
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' https://agents.example.com:7656/api/login # 404
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' https://agents.example.com:7656/ # 404
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' https://agents.example.com:7656/api/login # 404
|
||||
```
|
||||
|
||||
When commands are enabled, also verify that `/api/agent/ws` reaches Pulse
|
||||
@@ -123,33 +149,45 @@ the tunnels and keep the management port out of them.
|
||||
|
||||
2. **Management port is not reachable from a client site.** From a client
|
||||
network (or through a client tunnel), a connection to `FRONTEND_PORT` must
|
||||
time out or be refused by your firewall — not answer.
|
||||
time out or be refused by your firewall — not answer. That observation alone
|
||||
does not distinguish a firewall from a DNS, TLS or server failure; confirm
|
||||
the same management endpoint works from the authorised staff network.
|
||||
|
||||
3. **Agent tokens cannot manage.** A request to a management endpoint with an
|
||||
agent token must be rejected:
|
||||
3. **Agent tokens cannot manage.** On an authentication-enabled instance, a
|
||||
request to a management endpoint with the agent token must be rejected:
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' \
|
||||
-H "X-API-Token: <agent:report token>" https://pulse.internal:7655/api/notifications/webhooks # 401/403
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' \
|
||||
--header "@$HOME/.config/pulse/agent-header" \
|
||||
https://pulse.internal:7655/api/notifications/webhooks # 401/403
|
||||
```
|
||||
|
||||
4. **Cross-tenant isolation (shared-process mode only).** A token bound to one
|
||||
organization must get `403` when targeting another organization AND when
|
||||
targeting the default org (a leaked client-site token must not read the
|
||||
provider's own estate):
|
||||
4. **Cross-tenant isolation (shared-process mode only).** A valid token bound
|
||||
to org-A with `monitoring:read` must get `403` when targeting another
|
||||
organization AND when targeting the default org (a leaked client-site
|
||||
token must not read the provider's own estate):
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' \
|
||||
-H "X-API-Token: <org-A token>" -H "X-Pulse-Org-ID: org-b" \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' \
|
||||
-H "X-API-Token: <org-A token>" -H "X-Pulse-Org-ID: default" \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' \
|
||||
--header "@$HOME/.config/pulse/org-a-header" --header 'X-Pulse-Org-ID: org-b' \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' \
|
||||
--header "@$HOME/.config/pulse/org-a-header" --header 'X-Pulse-Org-ID: default' \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
```
|
||||
|
||||
An expired or otherwise invalid token can give `401` before the tenant
|
||||
boundary is checked. First confirm that this same token can read
|
||||
`/api/alerts/active` in org-A through the private header-file method; do not
|
||||
count an authentication failure as cross-tenant acceptance.
|
||||
|
||||
Keep your own monitoring estate in its own organization too, rather than
|
||||
in the default org, so every boundary in the instance is an explicit org
|
||||
boundary.
|
||||
boundary. For help, share only the check, redacted endpoint role, status and
|
||||
curl exit; never the header file, token, response body or trace.
|
||||
|
||||
## Connecting a client's Proxmox or PBS over your VPN
|
||||
|
||||
@@ -169,8 +207,11 @@ Per client, the steps are:
|
||||
|
||||
1. Make the client's PVE/PBS API address reachable from the Docker host that
|
||||
runs the client workspaces (route or interface into that client's
|
||||
tunnel). From the host, `curl -sk https://<client-pve>:8006` should
|
||||
answer before you involve Pulse.
|
||||
tunnel). From that host, make a bounded unauthenticated HTTPS request to
|
||||
the PVE/PBS API with certificate verification enabled. For a private or
|
||||
self-signed certificate, use `--cacert` with an independently verified
|
||||
certificate/CA. A TLS error is not proof of a blocked tunnel, and an HTTP
|
||||
response is not proof that Pulse's configured account can collect data.
|
||||
2. Open the client's workspace (portal → workspace → **Open**) and add the
|
||||
node under **Settings → Infrastructure**, using the tunnel-reachable
|
||||
address. The guided flow generates a setup command to run once on the
|
||||
|
||||
@@ -110,11 +110,37 @@ the tunnels and keep the management port out of them.
|
||||
|
||||
### Validation checklist (run after setup, repeat after network changes)
|
||||
|
||||
Use your actual HTTPS addresses and keep certificate verification enabled.
|
||||
For a private CA, add `--cacert /path/to/trusted-ca.pem` using a CA you have
|
||||
verified independently; do not bypass a certificate error with `--insecure`.
|
||||
The examples make bounded **GET** requests, discard response bodies and print
|
||||
only the HTTP status. They do not send notifications or change settings.
|
||||
|
||||
For the authenticated checks, prepare two separate private header files with
|
||||
[the API token file procedure](API.md#api-token-recommended), substituting
|
||||
`agent-header` and `org-a-header` for `api-header`. Keep the directory mode
|
||||
`700` and files mode `600`, owned by the account running curl. Enter the token
|
||||
only in your local editor, never in a shell command, URL or report. Each file
|
||||
contains one `X-API-Token` header: the existing `agent:report` token in
|
||||
`agent-header`, and an existing org-A token with `monitoring:read` in
|
||||
`org-a-header`. Do not broaden scopes or create a privileged token just to run
|
||||
these checks. Keep the files outside repositories, backups shared for support
|
||||
and diagnostic exports; remove them locally when no longer needed.
|
||||
|
||||
Keep `--disable` first so a local curl configuration cannot enable tracing,
|
||||
extra credentials or redirects. Do not add verbose/trace output or follow
|
||||
redirects with these tokens. For the HTTP-status checks below, a TLS, DNS,
|
||||
timeout or connection error is a **failed check**, not proof of isolation:
|
||||
require curl exit **0** as well as the expected HTTP status. `000` is not an HTTP result. Do not use `--fail` for these
|
||||
checks because the expected rejections are HTTP errors.
|
||||
|
||||
1. **Agent port excludes management surfaces.** Both must return `404`:
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' https://agents.example.com:7656/ # 404
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' https://agents.example.com:7656/api/login # 404
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' https://agents.example.com:7656/ # 404
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' https://agents.example.com:7656/api/login # 404
|
||||
```
|
||||
|
||||
When commands are enabled, also verify that `/api/agent/ws` reaches Pulse
|
||||
@@ -123,33 +149,45 @@ the tunnels and keep the management port out of them.
|
||||
|
||||
2. **Management port is not reachable from a client site.** From a client
|
||||
network (or through a client tunnel), a connection to `FRONTEND_PORT` must
|
||||
time out or be refused by your firewall — not answer.
|
||||
time out or be refused by your firewall — not answer. That observation alone
|
||||
does not distinguish a firewall from a DNS, TLS or server failure; confirm
|
||||
the same management endpoint works from the authorised staff network.
|
||||
|
||||
3. **Agent tokens cannot manage.** A request to a management endpoint with an
|
||||
agent token must be rejected:
|
||||
3. **Agent tokens cannot manage.** On an authentication-enabled instance, a
|
||||
request to a management endpoint with the agent token must be rejected:
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' \
|
||||
-H "X-API-Token: <agent:report token>" https://pulse.internal:7655/api/notifications/webhooks # 401/403
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' \
|
||||
--header "@$HOME/.config/pulse/agent-header" \
|
||||
https://pulse.internal:7655/api/notifications/webhooks # 401/403
|
||||
```
|
||||
|
||||
4. **Cross-tenant isolation (shared-process mode only).** A token bound to one
|
||||
organization must get `403` when targeting another organization AND when
|
||||
targeting the default org (a leaked client-site token must not read the
|
||||
provider's own estate):
|
||||
4. **Cross-tenant isolation (shared-process mode only).** A valid token bound
|
||||
to org-A with `monitoring:read` must get `403` when targeting another
|
||||
organization AND when targeting the default org (a leaked client-site
|
||||
token must not read the provider's own estate):
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' \
|
||||
-H "X-API-Token: <org-A token>" -H "X-Pulse-Org-ID: org-b" \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' \
|
||||
-H "X-API-Token: <org-A token>" -H "X-Pulse-Org-ID: default" \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' \
|
||||
--header "@$HOME/.config/pulse/org-a-header" --header 'X-Pulse-Org-ID: org-b' \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
curl --disable --silent --show-error --proto '=https' --connect-timeout 5 --max-time 10 \
|
||||
--output /dev/null --write-out '%{http_code}\n' \
|
||||
--header "@$HOME/.config/pulse/org-a-header" --header 'X-Pulse-Org-ID: default' \
|
||||
https://pulse.internal:7655/api/alerts/active # 403
|
||||
```
|
||||
|
||||
An expired or otherwise invalid token can give `401` before the tenant
|
||||
boundary is checked. First confirm that this same token can read
|
||||
`/api/alerts/active` in org-A through the private header-file method; do not
|
||||
count an authentication failure as cross-tenant acceptance.
|
||||
|
||||
Keep your own monitoring estate in its own organization too, rather than
|
||||
in the default org, so every boundary in the instance is an explicit org
|
||||
boundary.
|
||||
boundary. For help, share only the check, redacted endpoint role, status and
|
||||
curl exit; never the header file, token, response body or trace.
|
||||
|
||||
## Connecting a client's Proxmox or PBS over your VPN
|
||||
|
||||
@@ -169,8 +207,11 @@ Per client, the steps are:
|
||||
|
||||
1. Make the client's PVE/PBS API address reachable from the Docker host that
|
||||
runs the client workspaces (route or interface into that client's
|
||||
tunnel). From the host, `curl -sk https://<client-pve>:8006` should
|
||||
answer before you involve Pulse.
|
||||
tunnel). From that host, make a bounded unauthenticated HTTPS request to
|
||||
the PVE/PBS API with certificate verification enabled. For a private or
|
||||
self-signed certificate, use `--cacert` with an independently verified
|
||||
certificate/CA. A TLS error is not proof of a blocked tunnel, and an HTTP
|
||||
response is not proof that Pulse's configured account can collect data.
|
||||
2. Open the client's workspace (portal → workspace → **Open**) and add the
|
||||
node under **Settings → Infrastructure**, using the tunnel-reachable
|
||||
address. The guided flow generates a setup command to run once on the
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
package installtests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Exercise the copied recipes against a synthetic TLS peer, not a customer's
|
||||
// management API. This proves curl transport/credential handling, not Pulse's
|
||||
// tenant authorisation or an installed provider deployment.
|
||||
func mspValidationRecipes(t *testing.T) []string {
|
||||
t.Helper()
|
||||
doc, err := os.ReadFile(repoFile("docs", "MSP.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const heading = "### Validation checklist (run after setup, repeat after network changes)\n"
|
||||
_, section, found := strings.Cut(string(doc), heading)
|
||||
if !found {
|
||||
t.Fatal("MSP validation checklist is missing")
|
||||
}
|
||||
section, _, _ = strings.Cut(section, "\n## ")
|
||||
blocks := regexp.MustCompile("(?s)```bash\n(.*?)```").FindAllStringSubmatch(section, -1)
|
||||
var recipes []string
|
||||
for _, block := range blocks {
|
||||
// A continuation is one command; test each command separately so the
|
||||
// second public probe cannot mask the first probe's failed exit.
|
||||
joined := regexp.MustCompile(`\\\n[ \t]*`).ReplaceAllString(block[1], " ")
|
||||
for _, line := range strings.Split(joined, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if err := mspRecipeSafety(line); err != nil {
|
||||
t.Fatalf("unsafe copied MSP recipe: %v", err)
|
||||
}
|
||||
recipes = append(recipes, line)
|
||||
}
|
||||
}
|
||||
if len(recipes) != 5 {
|
||||
t.Fatalf("want five HTTP checks, got %d", len(recipes))
|
||||
}
|
||||
return recipes
|
||||
}
|
||||
|
||||
func mspRecipeSafety(recipe string) error {
|
||||
for _, required := range []string{
|
||||
"curl --disable ", "--silent --show-error", "--proto '=https'",
|
||||
"--connect-timeout 5", "--max-time 10", "--output /dev/null",
|
||||
"--write-out '%{http_code}\\n'", "https://",
|
||||
} {
|
||||
if !strings.Contains(recipe, required) {
|
||||
return fmt.Errorf("missing %s", required)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{
|
||||
"--insecure", "--location", "--config", "--trace", "--verbose",
|
||||
"--fail", "X-API-Token:", "Authorization:", "--data", "--request",
|
||||
} {
|
||||
if strings.Contains(recipe, forbidden) {
|
||||
return fmt.Errorf("forbidden %s", forbidden)
|
||||
}
|
||||
}
|
||||
if regexp.MustCompile(`(?:^|\s)-[A-Za-z]`).MatchString(recipe) ||
|
||||
strings.ContainsAny(recipe, "|;`") || strings.Contains(recipe, "$(") {
|
||||
return fmt.Errorf("unexpected short flag, pipeline or shell expansion")
|
||||
}
|
||||
if strings.Contains(recipe, "pulse.internal") &&
|
||||
!strings.Contains(recipe, `--header "@$HOME/.config/pulse/`) {
|
||||
return fmt.Errorf("authenticated request lacks private header file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMSPDiagnosticGuidanceRejectsUnsafeRecipes(t *testing.T) {
|
||||
recipe := mspValidationRecipes(t)[2]
|
||||
for name, unsafe := range map[string]string{
|
||||
"curl config": strings.Replace(recipe, "--disable ", "", 1),
|
||||
"TLS bypass": strings.Replace(recipe, "--disable ", "--disable --insecure ", 1),
|
||||
"short bypass": strings.Replace(recipe, "--disable ", "--disable -k ", 1),
|
||||
"redirect": strings.Replace(recipe, "--disable ", "--disable --location ", 1),
|
||||
"trace": strings.Replace(recipe, "--disable ", "--disable --trace /tmp/trace ", 1),
|
||||
"inline token": strings.Replace(recipe, `"@$HOME/.config/pulse/agent-header"`,
|
||||
`"X-API-Token: synthetic-inline-secret"`, 1),
|
||||
"missing deadline": strings.Replace(recipe, "--max-time 10 ", "", 1),
|
||||
"missing connect bound": strings.Replace(recipe, "--connect-timeout 5 ", "", 1),
|
||||
"body disclosure": strings.Replace(recipe, "--output /dev/null ", "", 1),
|
||||
"HTTP": strings.Replace(recipe, "https://", "http://", 1),
|
||||
"pipe hides exit": recipe + " | cat",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if mspRecipeSafety(unsafe) == nil {
|
||||
t.Fatal("unsafe mutation was accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mspProbeRequest struct {
|
||||
method, path, token, org string
|
||||
}
|
||||
|
||||
type mspProbeLab struct {
|
||||
home, ca, capture, trace, curl string
|
||||
server *httptest.Server
|
||||
requests chan mspProbeRequest
|
||||
}
|
||||
|
||||
func newMSPProbeLab(t *testing.T, redirect bool) *mspProbeLab {
|
||||
t.Helper()
|
||||
realCurl, err := exec.LookPath("curl")
|
||||
if err != nil {
|
||||
t.Fatal("curl is required to verify copied MSP diagnostics")
|
||||
}
|
||||
lab := &mspProbeLab{home: t.TempDir(), curl: realCurl, requests: make(chan mspProbeRequest, 8)}
|
||||
lab.ca = filepath.Join(lab.home, "trusted-ca.pem")
|
||||
lab.capture = filepath.Join(lab.home, "argv")
|
||||
lab.trace = filepath.Join(lab.home, "forbidden-trace")
|
||||
lab.server = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
lab.requests <- mspProbeRequest{r.Method, r.URL.Path, r.Header.Get("X-API-Token"), r.Header.Get("X-Pulse-Org-ID")}
|
||||
if redirect {
|
||||
http.Redirect(w, r, lab.server.URL+"/must-not-follow", http.StatusFound)
|
||||
return
|
||||
}
|
||||
code := http.StatusForbidden
|
||||
if r.URL.Path == "/" || r.URL.Path == "/api/login" {
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
// Even an upstream body echoing credentials must be discarded.
|
||||
fmt.Fprint(w, "synthetic-agent-secret synthetic-org-secret private-body")
|
||||
}))
|
||||
lab.server.Config.ErrorLog = log.New(io.Discard, "", 0)
|
||||
lab.server.StartTLS()
|
||||
t.Cleanup(lab.server.Close)
|
||||
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: lab.server.Certificate().Raw})
|
||||
if err := os.WriteFile(lab.ca, cert, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := filepath.Join(lab.home, ".config", "pulse")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, token := range map[string]string{"agent-header": "synthetic-agent-secret", "org-a-header": "synthetic-org-secret"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte("X-API-Token: "+token+"\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// --disable must ignore settings that would disclose headers, bypass TLS
|
||||
// or follow redirects, even on a user's otherwise configured curl.
|
||||
if err := os.WriteFile(filepath.Join(lab.home, ".curlrc"), []byte("insecure\nlocation\ntrace = "+lab.trace+"\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin := filepath.Join(lab.home, "bin")
|
||||
if err := os.Mkdir(bin, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapper := "#!/bin/sh\nprintf '%s\\0' \"$@\" > \"$MSP_ARGV\"\nexec \"$MSP_CURL\" \"$@\"\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, "curl"), []byte(wrapper), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return lab
|
||||
}
|
||||
|
||||
func (lab *mspProbeLab) run(t *testing.T, recipe string, trustCA bool) (string, int) {
|
||||
t.Helper()
|
||||
// Replace only documented endpoint placeholders; the CA option is the
|
||||
// guide's explicit private-CA alternative. Credentials never enter argv.
|
||||
recipe = strings.ReplaceAll(recipe, "https://agents.example.com:7656", lab.server.URL)
|
||||
recipe = strings.ReplaceAll(recipe, "https://pulse.internal:7655", lab.server.URL)
|
||||
if trustCA {
|
||||
recipe = strings.Replace(recipe, "curl --disable ", `curl --disable --cacert "$MSP_CA" `, 1)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
command := exec.CommandContext(ctx, "bash", "-c", recipe)
|
||||
command.Env = append(os.Environ(), "HOME="+lab.home, "PATH="+filepath.Join(lab.home, "bin")+":"+os.Getenv("PATH"),
|
||||
"MSP_ARGV="+lab.capture, "MSP_CURL="+lab.curl, "MSP_CA="+lab.ca,
|
||||
"https_proxy=", "HTTPS_PROXY=", "http_proxy=", "HTTP_PROXY=", "ALL_PROXY=", "all_proxy=")
|
||||
output, err := command.CombinedOutput()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("copied recipe exceeded test deadline: %v", ctx.Err())
|
||||
}
|
||||
exit := 0
|
||||
if err != nil {
|
||||
failure, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exit = failure.ExitCode()
|
||||
}
|
||||
argv, err := os.ReadFile(lab.capture)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range []string{"synthetic-agent-secret", "synthetic-org-secret", "private-body"} {
|
||||
if strings.Contains(string(argv), secret) || strings.Contains(string(output), secret) {
|
||||
t.Fatal("copied recipe disclosed synthetic private data")
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(lab.trace); !os.IsNotExist(err) {
|
||||
t.Fatal("curl local configuration enabled a trace")
|
||||
}
|
||||
return string(output), exit
|
||||
}
|
||||
|
||||
func TestMSPDiagnosticGuidanceCopiedChecksUsePrivateHeadersAndTrustedTLS(t *testing.T) {
|
||||
lab := newMSPProbeLab(t, false)
|
||||
want := []mspProbeRequest{
|
||||
{"GET", "/", "", ""}, {"GET", "/api/login", "", ""},
|
||||
{"GET", "/api/notifications/webhooks", "synthetic-agent-secret", ""},
|
||||
{"GET", "/api/alerts/active", "synthetic-org-secret", "org-b"},
|
||||
{"GET", "/api/alerts/active", "synthetic-org-secret", "default"},
|
||||
}
|
||||
for i, recipe := range mspValidationRecipes(t) {
|
||||
t.Run(fmt.Sprint(i+1), func(t *testing.T) {
|
||||
output, exit := lab.run(t, recipe, true)
|
||||
expected := "403\n"
|
||||
if i < 2 {
|
||||
expected = "404\n"
|
||||
}
|
||||
if exit != 0 || output != expected {
|
||||
t.Fatalf("curl result = %q, exit %d; want %q, exit 0", output, exit, expected)
|
||||
}
|
||||
select {
|
||||
case got := <-lab.requests:
|
||||
if got != want[i] {
|
||||
t.Fatal("request method, path or private header did not match the documented check")
|
||||
}
|
||||
default:
|
||||
t.Fatal("copied check sent no request")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMSPDiagnosticGuidanceUntrustedTLSRemainsFailure(t *testing.T) {
|
||||
lab := newMSPProbeLab(t, false)
|
||||
output, exit := lab.run(t, mspValidationRecipes(t)[2], false)
|
||||
if exit != 60 || !strings.HasSuffix(output, "000\n") {
|
||||
t.Fatalf("want certificate failure/000, got exit %d, %q", exit, output)
|
||||
}
|
||||
if len(lab.requests) != 0 {
|
||||
t.Fatal("untrusted peer received an authenticated HTTP request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMSPDiagnosticGuidanceDoesNotFollowRedirects(t *testing.T) {
|
||||
lab := newMSPProbeLab(t, true)
|
||||
output, exit := lab.run(t, mspValidationRecipes(t)[2], true)
|
||||
if exit != 0 || output != "302\n" || len(lab.requests) != 1 {
|
||||
t.Fatalf("redirect must remain an unexpected status, not a followed check: %q, exit %d", output, exit)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user