diff --git a/.golangci.yml b/.golangci.yml index 2e1ae59..39d4b0f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -71,6 +71,15 @@ linters: - linters: - gosec text: "G703" + # G115 (integer overflow on int->uintptr) ships only in gosec newer than + # our pinned CI golangci-lint v2.4, so match by text (a typed exclude would + # fail `config verify` there, like G706/G703 above). The broker converts + # file descriptors from socketpair / ParseUnixRights to uintptr for + # os.NewFile; an fd is a small non-negative int and never overflows uintptr. + - linters: + - gosec + text: "G115" + path: environment/broker_linux formatters: enable: diff --git a/environment/broker_e2e_linux_test.go b/environment/broker_e2e_linux_test.go new file mode 100644 index 0000000..01726f4 --- /dev/null +++ b/environment/broker_e2e_linux_test.go @@ -0,0 +1,101 @@ +//go:build linux + +package environment + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/flashcatcloud/flashduty-runner/permission" + "github.com/flashcatcloud/flashduty-runner/protocol" +) + +// TestBrokerE2E_RealFduty drives the REAL fduty CLI through the runner broker to +// a REAL upstream (the local pgy). Opt-in: it only runs when EGRESS_E2E=1 with a +// real key + reachable base + the linux fduty staged in FDUTY_BIN. It is the +// deterministic end-to-end proof that the broker injects the real key (auth +// succeeds), the key never reaches the bash env, and concurrent fduty calls each +// get their own dispatched connection. +// +// Run (from the host, after building the linux test binary + fduty): +// +// GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go test -c -o /tmp/egress-e2e/broker.test ./environment/ +// docker run --rm -v /tmp/egress-e2e:/app -w /app \ +// -e EGRESS_E2E=1 -e FDUTY_KEY=... -e FDUTY_BASE=http://host.docker.internal:11480 -e FDUTY_BIN=/app \ +// ubuntu:22.04 ./broker.test -test.run TestBrokerE2E_RealFduty -test.v +func TestBrokerE2E_RealFduty(t *testing.T) { + if os.Getenv("EGRESS_E2E") != "1" { + t.Skip("set EGRESS_E2E=1 with FDUTY_KEY + FDUTY_BASE + FDUTY_BIN to run") + } + key := os.Getenv("FDUTY_KEY") + base := os.Getenv("FDUTY_BASE") + binDir := os.Getenv("FDUTY_BIN") + if key == "" || base == "" || binDir == "" { + t.Fatal("FDUTY_KEY, FDUTY_BASE, FDUTY_BIN are all required") + } + t.Setenv("FLASHDUTY_RUNNER_BIN_DIR", binDir) + + checker := permission.NewChecker(map[string]string{"*": "allow"}) + e, err := New(t.TempDir(), checker) + if err != nil { + t.Fatalf("New environment: %v", err) + } + cred := &protocol.BashCredential{Key: key, BaseURL: base} + ctx := context.Background() + + // (a) Real authenticated read through the broker. A working key returns + // channel rows; a broken broker would surface a 401 / "app_key invalid". + res, err := e.executeBashCommand(ctx, + "fduty channel list 2>&1 | head -c 600", t.TempDir(), 60*time.Second, nil, cred) + if err != nil { + t.Fatalf("bash (channel list): %v", err) + } + t.Logf("broker fduty output:\n%s", res.Stdout) + low := strings.ToLower(res.Stdout) + if strings.Contains(low, "app_key") && strings.Contains(low, "invalid") { + t.Fatalf("auth FAILED through broker (sentinel not overwritten?): %s", res.Stdout) + } + if strings.Contains(res.Stdout, "401") || strings.Contains(low, "unauthorized") { + t.Fatalf("auth FAILED through broker (401): %s", res.Stdout) + } + if strings.TrimSpace(res.Stdout) == "" { + t.Fatalf("empty output through broker — expected channel rows") + } + + // (b) The real key must NEVER be visible in the bash environment. + leak, err := e.executeBashCommand(ctx, + "env | grep -i FLASHDUTY_APP_KEY || echo NO_KEY", t.TempDir(), 30*time.Second, nil, cred) + if err != nil { + t.Fatalf("bash (env leak check): %v", err) + } + if !strings.Contains(leak.Stdout, "NO_KEY") || strings.Contains(leak.Stdout, key) { + t.Fatalf("app_key LEAKED into bash env: %q", leak.Stdout) + } + if !strings.Contains(leak.Stdout, "FLASHDUTY_CRED_FD") { + // sanity: broker mode really engaged (CRED_FD is set) + fd, _ := e.executeBashCommand(ctx, "echo CRED_FD=$FLASHDUTY_CRED_FD BASE=$FLASHDUTY_BASE_URL", + t.TempDir(), 30*time.Second, nil, cred) + t.Logf("broker env sanity: %s", strings.TrimSpace(fd.Stdout)) + } + + // (c) Concurrency: two fduty in one bash, each gets its own dispatched + // SOCK_STREAM connection; both must authenticate (no byte interleave). + conc, err := e.executeBashCommand(ctx, + "fduty channel list >/tmp/a 2>&1 & fduty channel list >/tmp/b 2>&1 & wait; "+ + "echo A=$(wc -l