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
11 changes: 11 additions & 0 deletions pkg/cmd/deregister/deregister.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type deregisterDeps struct {
platform externalnode.PlatformChecker
prompter terminal.Selector
netbird register.NetBirdManager
sshd register.ManagedSSHDaemon
nodeClients externalnode.NodeClientFactory
registrationStore register.RegistrationStore
sshKeys SSHKeyRemover
Expand All @@ -58,6 +59,7 @@ func defaultDeregisterDeps(brevHome string) deregisterDeps {
platform: register.LinuxPlatform{},
prompter: register.TerminalPrompter{},
netbird: register.Netbird{},
sshd: register.BrevSSHD{},
nodeClients: register.DefaultNodeClientFactory{},
registrationStore: register.NewFileRegistrationStore(brevHome),
sshKeys: brevSSHKeyRemover{},
Expand Down Expand Up @@ -158,6 +160,15 @@ func runDeregister(ctx context.Context, t *terminal.Terminal, s DeregisterStore,
}
t.Vprint("")

// Remove brev-managed sshd (non-fatal on failure).
t.Vprint("Removing managed SSH daemon...")
if err := deps.sshd.Uninstall(); err != nil {
t.Vprintf(" Warning: failed to remove managed SSH daemon: %v\n", err)
} else {
t.Vprint(t.Green(" Managed SSH daemon removed."))
}
t.Vprint("")

t.Vprint("Removing Brev tunnel...")
if err := deps.netbird.Uninstall(); err != nil {
t.Vprintf(" Warning: failed to remove Brev tunnel: %v\n", err)
Expand Down
55 changes: 55 additions & 0 deletions pkg/cmd/deregister/deregister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ type mockNetBirdManager struct {
func (m *mockNetBirdManager) Install() error { return m.err }
func (m *mockNetBirdManager) Uninstall() error { m.called = true; return m.err }

type mockManagedSSHDaemon struct {
uninstallCalled bool
uninstallErr error
}

func (m *mockManagedSSHDaemon) Install() error { return nil }
func (m *mockManagedSSHDaemon) Uninstall() error {
m.uninstallCalled = true
return m.uninstallErr
}

type mockNodeClientFactory struct {
serverURL string
}
Expand Down Expand Up @@ -133,6 +144,7 @@ func testDeregisterDeps(t *testing.T, svc *fakeNodeService, regStore register.Re
return ""
}},
netbird: &mockNetBirdManager{},
sshd: &mockManagedSSHDaemon{},
nodeClients: mockNodeClientFactory{serverURL: server.URL},
registrationStore: regStore,
sshKeys: &mockSSHKeyRemover{},
Expand Down Expand Up @@ -379,3 +391,46 @@ func Test_runDeregister_RemoveBrevKeysHandling(t *testing.T) {
})
}
}

func Test_runDeregister_SSHDUninstallFailureIsNonFatal(t *testing.T) {
regStore := &mockRegistrationStore{
reg: &register.DeviceRegistration{
ExternalNodeID: "unode_abc",
DisplayName: "My Spark",
OrgID: "org_123",
},
}

store := &mockDeregisterStore{
user: &entity.User{ID: "user_1"},
home: "/home/testuser/.brev",
token: "tok",
}

svc := &fakeNodeService{
removeNodeFn: func(_ *nodev1.RemoveNodeRequest) (*nodev1.RemoveNodeResponse, error) {
return &nodev1.RemoveNodeResponse{}, nil
},
}

sshdMock := &mockManagedSSHDaemon{uninstallErr: fmt.Errorf("permission denied")}
deps, server := testDeregisterDeps(t, svc, regStore)
defer server.Close()
deps.sshd = sshdMock

term := terminal.New()
err := runDeregister(context.Background(), term, store, deps)
if err != nil {
t.Fatalf("expected nil error (sshd failure should be non-fatal), got: %v", err)
}

if !sshdMock.uninstallCalled {
t.Error("expected sshd Uninstall to be called")
}

// Registration should still be cleaned up despite sshd failure.
exists, _ := regStore.Exists()
if exists {
t.Error("expected registration to be deleted")
}
}
6 changes: 6 additions & 0 deletions pkg/cmd/register/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ type Netbird struct{}
func (Netbird) Install() error { return InstallNetbird() }
func (Netbird) Uninstall() error { return UninstallNetbird() }

// BrevSSHD manages the brev-managed sshd instance on port 2222.
type BrevSSHD struct{}

func (BrevSSHD) Install() error { return InstallBrevSSHD() }
func (BrevSSHD) Uninstall() error { return UninstallBrevSSHD() }

// ShellSetupRunner runs setup scripts via shell.
type ShellSetupRunner struct{}

Expand Down
26 changes: 21 additions & 5 deletions pkg/cmd/register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ type NetBirdManager interface {
Uninstall() error
}

// ManagedSSHDaemon installs and uninstalls a brev-managed sshd instance.
type ManagedSSHDaemon interface {
Install() error
Uninstall() error
}

// SetupRunner runs a setup script on the local machine.
type SetupRunner interface {
RunSetup(script string) error
Expand All @@ -59,6 +65,7 @@ type registerDeps struct {
platform externalnode.PlatformChecker
prompter terminal.Confirmer
netbird NetBirdManager
sshd ManagedSSHDaemon
setupRunner SetupRunner
nodeClients externalnode.NodeClientFactory
commandRunner CommandRunner
Expand All @@ -71,6 +78,7 @@ func defaultRegisterDeps(brevHome string) registerDeps {
platform: LinuxPlatform{},
prompter: TerminalPrompter{},
netbird: Netbird{},
sshd: BrevSSHD{},
setupRunner: ShellSetupRunner{},
nodeClients: DefaultNodeClientFactory{},
commandRunner: ExecCommandRunner{},
Expand Down Expand Up @@ -147,8 +155,9 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam
t.Vprint("")
t.Vprint("This will perform the following steps:")
t.Vprint(" 1. Set up Brev tunnel")
t.Vprint(" 2. Collect hardware profile")
t.Vprint(" 3. Register this machine with Brev")
t.Vprint(" 2. Set up managed SSH daemon (port 2222)")
t.Vprint(" 3. Collect hardware profile")
t.Vprint(" 4. Register this machine with Brev")
t.Vprint("")

if !deps.prompter.ConfirmYesNo("Proceed with registration?") {
Expand All @@ -157,14 +166,21 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam
}

t.Vprint("")
t.Vprint(t.Yellow("[Step 1/3] Setting up Brev tunnel..."))
t.Vprint(t.Yellow("[Step 1/4] Setting up Brev tunnel..."))
if err := deps.netbird.Install(); err != nil {
return fmt.Errorf("brev tunnel setup failed: %w", err)
}
t.Vprint(t.Green(" Brev tunnel ready."))

t.Vprint("")
t.Vprint(t.Yellow("[Step 2/3] Collecting hardware profile..."))
t.Vprint(t.Yellow("[Step 2/4] Setting up managed SSH daemon..."))
if err := deps.sshd.Install(); err != nil {
return fmt.Errorf("managed sshd setup failed: %w", err)
}
t.Vprint(t.Green(" Managed SSH daemon ready (port 2222)."))

t.Vprint("")
t.Vprint(t.Yellow("[Step 3/4] Collecting hardware profile..."))
t.Vprint("")

nodeSpec, err := CollectHardwareProfile(deps.commandRunner, deps.fileReader)
Expand All @@ -176,7 +192,7 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam
t.Vprint(FormatNodeSpec(nodeSpec))

t.Vprint("")
t.Vprint(t.Yellow("[Step 3/3] Registering with Brev..."))
t.Vprint(t.Yellow("[Step 4/4] Registering with Brev..."))

deviceID := uuid.New().String()
client := deps.nodeClients.NewNodeClient(s, config.GlobalConfig.GetBrevPublicAPIURL())
Expand Down
49 changes: 49 additions & 0 deletions pkg/cmd/register/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ type mockNetBirdManager struct{ err error }
func (m mockNetBirdManager) Install() error { return m.err }
func (m mockNetBirdManager) Uninstall() error { return m.err }

type mockManagedSSHDaemon struct {
installCalled bool
uninstallCalled bool
installErr error
uninstallErr error
}

func (m *mockManagedSSHDaemon) Install() error {
m.installCalled = true
return m.installErr
}

func (m *mockManagedSSHDaemon) Uninstall() error {
m.uninstallCalled = true
return m.uninstallErr
}

type mockSetupRunner struct {
called bool
cmd string
Expand Down Expand Up @@ -112,6 +129,7 @@ func testRegisterDeps(t *testing.T, svc *fakeNodeService, regStore RegistrationS
platform: mockPlatform{compatible: true},
prompter: mockConfirmer{confirm: true},
netbird: mockNetBirdManager{},
sshd: &mockManagedSSHDaemon{},
setupRunner: &mockSetupRunner{},
nodeClients: mockNodeClientFactory{serverURL: server.URL},
commandRunner: &mockCommandRunner{
Expand Down Expand Up @@ -419,6 +437,37 @@ func Test_runRegister_NoSetupCommand(t *testing.T) {
}
}

func Test_runRegister_SSHDInstallFailAbortsRegistration(t *testing.T) {
regStore := &mockRegistrationStore{}

store := &mockRegisterStore{
user: &entity.User{ID: "user_1"},
org: &entity.Organization{ID: "org_123", Name: "TestOrg"},
home: "/home/testuser/.brev",
token: "tok",
}

svc := &fakeNodeService{}
deps, server := testRegisterDeps(t, svc, regStore)
defer server.Close()

deps.sshd = &mockManagedSSHDaemon{installErr: fmt.Errorf("permission denied")}

term := terminal.New()
err := runRegister(context.Background(), term, store, "My Spark", deps)
if err == nil {
t.Fatal("expected error when sshd install fails")
}
if !strings.Contains(err.Error(), "managed sshd setup failed") {
t.Errorf("unexpected error message: %v", err)
}

exists, _ := regStore.Exists()
if exists {
t.Error("registration should not exist after sshd install failure")
}
}

func Test_runSetupCommand_Validation(t *testing.T) {
tests := []struct {
name string
Expand Down
136 changes: 136 additions & 0 deletions pkg/cmd/register/sshd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package register

import (
"fmt"
"os"
"os/exec"
"path/filepath"
)

const (
brevSSHDDir = "/etc/brev-sshd"
brevSSHDConfigPath = "/etc/brev-sshd/sshd_config"
brevSSHDUnitPath = "/etc/systemd/system/brev-sshd.service"
brevSSHDHostKey = "/etc/brev-sshd/ssh_host_ed25519_key"
brevSSHDBinary = "/usr/sbin/sshd"
)

// sshdConfig is the hardened sshd_config for the brev-managed sshd on port 2222.
const sshdConfig = `# Brev-managed sshd configuration
# Do not edit — managed by brev register/deregister

# Non-standard port to avoid conflicting with the system sshd on port 22.
Port 2222

# Isolated ed25519 host key in brev's own directory for clean install/uninstall.
HostKey /etc/brev-sshd/ssh_host_ed25519_key

# Key-only authentication — brev manages keys via sshkeys.go.
PubkeyAuthentication yes
# Disable password auth to prevent brute-force attacks.
PasswordAuthentication no
# Disable PAM to ensure it can't re-enable password or keyboard-interactive auth.
UsePAM no
# Allow root login only via public key, never password.
PermitRootLogin prohibit-password
# Limit auth attempts per connection (default 6) to reduce brute-force window.
MaxAuthTries 3
# Disconnect unauthenticated sessions after 30s (default 120) to limit resource waste.
LoginGraceTime 30

# Reuse the same authorized_keys managed by sshkeys.go — no separate key store needed.
AuthorizedKeysFile %h/.ssh/authorized_keys

# Modern AEAD ciphers only; excludes legacy CBC and non-AEAD modes.
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
# Post-quantum hybrid KEX + standard curve25519; excludes weak DH groups.
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256
# Encrypt-then-MAC variants only; stronger than MAC-then-encrypt.
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com

# Dedicated PID file to avoid collision with system sshd's /run/sshd.pid.
PidFile /run/brev-sshd.pid
`

// sshdUnit is the systemd unit file for the brev-managed sshd.
// ExecStartPre validates the config before starting (fail-fast on typos).
// ExecStart runs sshd in foreground mode (-D) so systemd can supervise it.
// Restart=on-failure auto-recovers from crashes without restarting on clean exit.
const sshdUnit = `[Unit]
Description=Brev SSH Daemon (port 2222)
After=network.target

[Service]
Type=simple
ExecStartPre=/usr/sbin/sshd -t -f /etc/brev-sshd/sshd_config
ExecStart=/usr/sbin/sshd -D -f /etc/brev-sshd/sshd_config
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
`

// InstallBrevSSHD sets up the brev-managed sshd on port 2222.
// It creates the config directory, generates a host key (idempotent),
// writes the sshd_config and systemd unit, then enables and starts the service.
func InstallBrevSSHD() error {
// Create config directory
if err := os.MkdirAll(brevSSHDDir, 0o755); err != nil {
return fmt.Errorf("creating brev-sshd directory: %w", err)
}

// Generate ed25519 host key if it doesn't exist
if _, err := os.Stat(brevSSHDHostKey); os.IsNotExist(err) {
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", brevSSHDHostKey, "-N", "") // #nosec G204
if err := cmd.Run(); err != nil {
return fmt.Errorf("generating host key: %w", err)
}
}

// Write sshd_config
if err := os.WriteFile(brevSSHDConfigPath, []byte(sshdConfig), 0o644); err != nil {
return fmt.Errorf("writing sshd_config: %w", err)
}

// Write systemd unit
if err := os.WriteFile(brevSSHDUnitPath, []byte(sshdUnit), 0o644); err != nil {
return fmt.Errorf("writing systemd unit: %w", err)
}

// Reload systemd, enable and start the service
script := `sudo systemctl daemon-reload && sudo systemctl enable brev-sshd.service && sudo systemctl start brev-sshd.service`
cmd := exec.Command("bash", "-c", script) // #nosec G204
if err := cmd.Run(); err != nil {
return fmt.Errorf("enabling brev-sshd service: %w", err)
}

return nil
}

// UninstallBrevSSHD stops and removes the brev-managed sshd service and its
// configuration. Errors are best-effort — the function attempts all cleanup
// steps even if individual steps fail.
func UninstallBrevSSHD() error {
// Stop and disable the service (best-effort)
_ = exec.Command("bash", "-c", "sudo systemctl stop brev-sshd.service 2>/dev/null").Run() // #nosec G204

Check failure on line 116 in pkg/cmd/register/sshd.go

View workflow job for this annotation

GitHub Actions / ci (ubuntu-22.04)

File is not properly formatted (gofumpt)
_ = exec.Command("bash", "-c", "sudo systemctl disable brev-sshd.service 2>/dev/null").Run() // #nosec G204

// Remove systemd unit file
if err := os.Remove(brevSSHDUnitPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing systemd unit: %w", err)
}

// Reload systemd
_ = exec.Command("bash", "-c", "sudo systemctl daemon-reload").Run() // #nosec G204

// Remove config directory
if err := os.RemoveAll(brevSSHDDir); err != nil {
return fmt.Errorf("removing brev-sshd directory: %w", err)
}

// Remove PID file if leftover
_ = os.Remove(filepath.Join("/run", "brev-sshd.pid"))

return nil
}
Loading
Loading