Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions e2e/test/direct_listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package e2e
import (
"fmt"
"path/filepath"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -164,4 +165,61 @@ var _ = Describe("Direct Listener E2E Tests", Label("direct-listener"), Ordered,
"--tls-grpc-insecure", "--", "j", "power", "on")
Expect(err).To(HaveOccurred())
})

// --- Auth by default (auto-generated passphrase) ---

It("--unsafe-no-auth allows unauthenticated access", func() {
config := configPath("exporter-direct-listener.yaml")
tracker.StartDirectExporter(config, listenerPort, "", false)
WaitForDirectExporterReady(listenerPort, "")

out, err := Jmp("shell", "--tls-grpc", fmt.Sprintf("127.0.0.1:%d", listenerPort),
"--tls-grpc-insecure", "--", "j", "power", "on")
Expect(err).NotTo(HaveOccurred(), out)
})

It("auto-generated passphrase is printed to stderr and can be used to connect", func() {
config := configPath("exporter-direct-listener.yaml")
_, stderrBuf := tracker.StartDirectExporterAutoAuth(config, listenerPort)

// Wait for the auto-generated passphrase message to appear in stderr
Eventually(func() string {
return stderrBuf.String()
}, 15*time.Second, 500*time.Millisecond).Should(ContainSubstring("Generated random passphrase"))

// Extract the passphrase from the log message
var generatedPassphrase string
for _, line := range strings.Split(stderrBuf.String(), "\n") {
if strings.Contains(line, "Generated random passphrase") {
parts := strings.SplitN(line, ": ", 2)
if len(parts) == 2 {
generatedPassphrase = strings.TrimSpace(parts[1])
}
break
}
}
Expect(generatedPassphrase).NotTo(BeEmpty(), "could not extract generated passphrase from stderr")

// Wait for the port to be ready
WaitForDirectExporterPort(listenerPort)

// Verify that the extracted passphrase allows connection
out, err := Jmp("shell", "--tls-grpc", fmt.Sprintf("127.0.0.1:%d", listenerPort),
"--tls-grpc-insecure", "--passphrase", generatedPassphrase, "--", "j", "power", "on")
Expect(err).NotTo(HaveOccurred(), out)

// Verify that a wrong passphrase is rejected
_, err = Jmp("shell", "--tls-grpc", fmt.Sprintf("127.0.0.1:%d", listenerPort),
"--tls-grpc-insecure", "--passphrase", "wrong-passphrase", "--", "j", "power", "on")
Expect(err).To(HaveOccurred())
})

It("--passphrase and --unsafe-no-auth are mutually exclusive", func() {
_, err := Jmp("run", "--exporter-config", configPath("exporter-direct-listener.yaml"),
"--tls-grpc-listener", fmt.Sprintf("%d", listenerPort),
"--tls-grpc-insecure",
"--passphrase", "my-secret",
"--unsafe-no-auth")
Expect(err).To(HaveOccurred())
})
})
22 changes: 22 additions & 0 deletions e2e/test/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,13 +392,31 @@ func (pt *ProcessTracker) StartExporterSingle(exporterName string) *exec.Cmd {
}

// StartDirectExporter starts an exporter with --tls-grpc-listener (direct mode).
// When no passphrase is provided, --unsafe-no-auth is passed automatically
// since authentication is required by default.
func (pt *ProcessTracker) StartDirectExporter(configFile string, port int, passphrase string, captureStderr bool) (*exec.Cmd, *logBuffer) {
return pt.startDirectExporter(configFile, port, passphrase, captureStderr, passphrase == "")
}

// StartDirectExporterAutoAuth starts an exporter in direct mode without
// --passphrase and without --unsafe-no-auth, so a random passphrase is
// auto-generated. Stderr is always captured so the caller can extract the
// generated passphrase from the log output.
func (pt *ProcessTracker) StartDirectExporterAutoAuth(configFile string, port int) (*exec.Cmd, *logBuffer) {
return pt.startDirectExporter(configFile, port, "", true, false)
Comment thread
raballew marked this conversation as resolved.
}

// startDirectExporter is the internal implementation for starting a direct exporter.
func (pt *ProcessTracker) startDirectExporter(configFile string, port int, passphrase string, captureStderr bool, unsafeNoAuth bool) (*exec.Cmd, *logBuffer) {
args := []string{"run", "--exporter-config", configFile,
"--tls-grpc-listener", strconv.Itoa(port),
"--tls-grpc-insecure"}
if passphrase != "" {
args = append(args, "--passphrase", passphrase)
}
if unsafeNoAuth {
args = append(args, "--unsafe-no-auth")
}

cmd := exec.Command(JmpPath(), args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
Expand Down Expand Up @@ -480,6 +498,10 @@ func (pt *ProcessTracker) StopAll() {
}
pt.pids = nil

// Reset log buffers so stale data from earlier tests does not
// accumulate and confuse debugging of subsequent test failures.
pt.logs = make(map[string]*logBuffer)

// Kill orphaned jmp exporter processes
_ = exec.Command("pkill", "-9", "-f", "jmp run --exporter").Run()
}
Expand Down
42 changes: 37 additions & 5 deletions python/packages/jumpstarter-cli/jumpstarter_cli/run.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import secrets
import signal
import sys

Expand Down Expand Up @@ -104,7 +105,7 @@ async def signal_handler():
if tls_insecure:
if passphrase:
click.echo(
"WARNING: --passphrase has no effect without TLS; "
"WARNING: passphrase authentication is active but TLS is disabled; "
"the passphrase will be transmitted in plaintext",
err=True,
)
Expand Down Expand Up @@ -245,16 +246,24 @@ def _serve_with_exc_handling(
"--passphrase",
"passphrase",
default=None,
help="Require this passphrase from clients connecting via --tls-grpc-listener.",
help="Require this passphrase from clients connecting via --tls-grpc-listener. "
"If not provided, a random passphrase is generated automatically.",
)
@click.option(
"--unsafe-no-auth",
"unsafe_no_auth",
is_flag=True,
help="Disable passphrase authentication entirely (dangerous: allows unauthenticated access).",
)
@handle_exceptions
def run(config, listener_bind, tls_insecure, tls_cert, tls_key, passphrase):
def run(config, listener_bind, tls_insecure, tls_cert, tls_key, passphrase, unsafe_no_auth):
"""Run an exporter locally."""
if listener_bind is not None and config is None:
raise click.UsageError("--exporter-config (or --exporter) is required when using --tls-grpc-listener")
if listener_bind is None and (tls_insecure or tls_cert or tls_key or passphrase):
if listener_bind is None and (tls_insecure or tls_cert or tls_key or passphrase or unsafe_no_auth):
raise click.UsageError(
"--tls-grpc-insecure, --tls-cert, --tls-key, and --passphrase require --tls-grpc-listener"
"--tls-grpc-insecure, --tls-cert, --tls-key, --passphrase, and --unsafe-no-auth "
"require --tls-grpc-listener"
)
if listener_bind is not None:
if tls_insecure and (tls_cert or tls_key):
Expand All @@ -263,5 +272,28 @@ def run(config, listener_bind, tls_insecure, tls_cert, tls_key, passphrase):
raise click.UsageError(
"--tls-grpc-listener requires either --tls-grpc-insecure or --tls-cert and --tls-key"
)
if passphrase and unsafe_no_auth:
raise click.UsageError("--passphrase and --unsafe-no-auth are mutually exclusive")

# Auto-generate a passphrase when none is provided and auth is not explicitly disabled
if not passphrase and not unsafe_no_auth:
passphrase = secrets.token_urlsafe(32)
click.echo(
f"Generated random passphrase (use --passphrase to set your own): {passphrase}",
Comment thread
raballew marked this conversation as resolved.
err=True,
)

if unsafe_no_auth and tls_insecure:
click.echo(
"WARNING: running without authentication AND without TLS. "
"The server is completely unprotected.",
err=True,
)
elif unsafe_no_auth:
click.echo(
"WARNING: running without authentication. "
"Any client with network access can control this exporter.",
err=True,
)
parsed_bind = _parse_listener_bind(listener_bind) if listener_bind is not None else None
return _serve_with_exc_handling(config, parsed_bind, tls_insecure, tls_cert, tls_key, passphrase)
Loading