From 1a5df5304a0d7e749ba00b556f1015f58c2591a4 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Tue, 3 Mar 2026 16:57:54 -0800 Subject: [PATCH 1/7] feat(register): add brev-managed sshd on port 2222 Add a hardened sshd instance managed by brev register/deregister that runs on port 2222 alongside the system sshd. Uses public key auth only, modern ciphers/KEX/MACs, isolated host keys in /etc/brev-sshd/, and a systemd unit for process supervision. - Add ManagedSSHDaemon interface + BrevSSHD provider - Wire sshd install as Step 2 in registration flow - Add non-fatal sshd cleanup to deregistration flow - Add config/unit content tests and mock-based integration tests --- pkg/cmd/deregister/deregister.go | 11 +++ pkg/cmd/deregister/deregister_test.go | 55 +++++++++++ pkg/cmd/register/providers.go | 6 ++ pkg/cmd/register/register.go | 26 ++++- pkg/cmd/register/register_test.go | 49 ++++++++++ pkg/cmd/register/sshd.go | 136 ++++++++++++++++++++++++++ pkg/cmd/register/sshd_test.go | 65 ++++++++++++ 7 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 pkg/cmd/register/sshd.go create mode 100644 pkg/cmd/register/sshd_test.go diff --git a/pkg/cmd/deregister/deregister.go b/pkg/cmd/deregister/deregister.go index 06b902dd..75defcae 100644 --- a/pkg/cmd/deregister/deregister.go +++ b/pkg/cmd/deregister/deregister.go @@ -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 @@ -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{}, @@ -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) diff --git a/pkg/cmd/deregister/deregister_test.go b/pkg/cmd/deregister/deregister_test.go index b901c9ff..2043febd 100644 --- a/pkg/cmd/deregister/deregister_test.go +++ b/pkg/cmd/deregister/deregister_test.go @@ -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 } @@ -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{}, @@ -379,3 +391,46 @@ func Test_runDeregister_RemoveBrevKeysHandling(t *testing.T) { }) } } + +func Test_runDeregister_SSHDUninstallFailureIsNonFatal(t *testing.T) { + regStore := &mockRegistrationStore{ + reg: ®ister.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") + } +} diff --git a/pkg/cmd/register/providers.go b/pkg/cmd/register/providers.go index cabfa1c4..07531e37 100644 --- a/pkg/cmd/register/providers.go +++ b/pkg/cmd/register/providers.go @@ -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{} diff --git a/pkg/cmd/register/register.go b/pkg/cmd/register/register.go index e56c0e6c..396e49e5 100644 --- a/pkg/cmd/register/register.go +++ b/pkg/cmd/register/register.go @@ -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 @@ -59,6 +65,7 @@ type registerDeps struct { platform externalnode.PlatformChecker prompter terminal.Confirmer netbird NetBirdManager + sshd ManagedSSHDaemon setupRunner SetupRunner nodeClients externalnode.NodeClientFactory commandRunner CommandRunner @@ -71,6 +78,7 @@ func defaultRegisterDeps(brevHome string) registerDeps { platform: LinuxPlatform{}, prompter: TerminalPrompter{}, netbird: Netbird{}, + sshd: BrevSSHD{}, setupRunner: ShellSetupRunner{}, nodeClients: DefaultNodeClientFactory{}, commandRunner: ExecCommandRunner{}, @@ -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?") { @@ -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) @@ -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()) diff --git a/pkg/cmd/register/register_test.go b/pkg/cmd/register/register_test.go index 5aad1b80..abc8cdbb 100644 --- a/pkg/cmd/register/register_test.go +++ b/pkg/cmd/register/register_test.go @@ -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 @@ -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{ @@ -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 diff --git a/pkg/cmd/register/sshd.go b/pkg/cmd/register/sshd.go new file mode 100644 index 00000000..bd39f866 --- /dev/null +++ b/pkg/cmd/register/sshd.go @@ -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 + _ = 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 +} diff --git a/pkg/cmd/register/sshd_test.go b/pkg/cmd/register/sshd_test.go new file mode 100644 index 00000000..40f2445f --- /dev/null +++ b/pkg/cmd/register/sshd_test.go @@ -0,0 +1,65 @@ +package register + +import ( + "strings" + "testing" +) + +func Test_sshdConfig_Content(t *testing.T) { + checks := []struct { + name string + want string + }{ + {"Port", "Port 2222"}, + {"HostKey", "HostKey /etc/brev-sshd/ssh_host_ed25519_key"}, + {"PubkeyAuth", "PubkeyAuthentication yes"}, + {"PasswordAuth", "PasswordAuthentication no"}, + {"UsePAM", "UsePAM no"}, + {"PermitRootLogin", "PermitRootLogin prohibit-password"}, + {"MaxAuthTries", "MaxAuthTries 3"}, + {"LoginGraceTime", "LoginGraceTime 30"}, + {"AuthorizedKeysFile", "AuthorizedKeysFile %h/.ssh/authorized_keys"}, + {"PidFile", "PidFile /run/brev-sshd.pid"}, + {"Chacha20Cipher", "chacha20-poly1305@openssh.com"}, + {"AES256GCM", "aes256-gcm@openssh.com"}, + {"AES128GCM", "aes128-gcm@openssh.com"}, + {"Sntrup761KEX", "sntrup761x25519-sha512@openssh.com"}, + {"Curve25519KEX", "curve25519-sha256"}, + {"HMACSHA256ETM", "hmac-sha2-256-etm@openssh.com"}, + {"HMACSHA512ETM", "hmac-sha2-512-etm@openssh.com"}, + } + + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + if !strings.Contains(sshdConfig, tc.want) { + t.Errorf("sshd_config missing %q", tc.want) + } + }) + } +} + +func Test_sshdUnit_Content(t *testing.T) { + checks := []struct { + name string + want string + }{ + {"Description", "Description=Brev SSH Daemon (port 2222)"}, + {"ExecStartPre", "ExecStartPre=/usr/sbin/sshd -t -f /etc/brev-sshd/sshd_config"}, + {"ExecStart", "ExecStart=/usr/sbin/sshd -D -f /etc/brev-sshd/sshd_config"}, + {"Restart", "Restart=on-failure"}, + {"WantedBy", "WantedBy=multi-user.target"}, + } + + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + if !strings.Contains(sshdUnit, tc.want) { + t.Errorf("systemd unit missing %q", tc.want) + } + }) + } +} + +func Test_BrevSSHD_ImplementsInterface(t *testing.T) { + // Compile-time check that BrevSSHD satisfies ManagedSSHDaemon. + var _ ManagedSSHDaemon = BrevSSHD{} +} From 026be693c6320af2aa45fa652d633d6c254b9f35 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 4 Mar 2026 14:47:25 -0800 Subject: [PATCH 2/7] feat(sshd): add AllowUsers management and improve error reporting - Wire AddAllowedUser/RemoveAllowedUser into GrantSSHAccessToNode with rollback on RPC failure - Add ReloadBrevSSHD to pick up config changes without dropping connections - Add brev-sshd to enablessh checkSSHDaemon service list - Split systemctl commands for better error messages and pipe stderr - Remove unused brevSSHDBinary const - Add tests for AllowUsers add/remove/idempotent/cleanup --- pkg/cmd/enablessh/enablessh.go | 2 +- pkg/cmd/register/sshd.go | 104 ++++++++++++++++++++++++++++++--- pkg/cmd/register/sshd_test.go | 93 +++++++++++++++++++++++++++++ pkg/cmd/register/sshkeys.go | 19 ++++++ 4 files changed, 210 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/enablessh/enablessh.go b/pkg/cmd/enablessh/enablessh.go index dac16bc8..4809e488 100644 --- a/pkg/cmd/enablessh/enablessh.go +++ b/pkg/cmd/enablessh/enablessh.go @@ -122,7 +122,7 @@ func enableSSH( // checkSSHDaemon prints a warning if neither "ssh" nor "sshd" systemd services // appear to be active. It never returns an error — it is best-effort. func checkSSHDaemon(t *terminal.Terminal) { - for _, svc := range []string{"ssh", "sshd"} { + for _, svc := range []string{"ssh", "sshd", "brev-sshd"} { out, err := exec.Command("systemctl", "is-active", svc).Output() //nolint:gosec // fixed service names if err == nil && len(out) > 0 && string(out[:len(out)-1]) == "active" { return diff --git a/pkg/cmd/register/sshd.go b/pkg/cmd/register/sshd.go index bd39f866..4af2e840 100644 --- a/pkg/cmd/register/sshd.go +++ b/pkg/cmd/register/sshd.go @@ -5,14 +5,14 @@ import ( "os" "os/exec" "path/filepath" + "strings" ) 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" + brevSSHDHostKey = "/etc/brev-sshd/ssh_host_ed25519_key" ) // sshdConfig is the hardened sshd_config for the brev-managed sshd on port 2222. @@ -98,11 +98,18 @@ func InstallBrevSSHD() error { 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) + // Reload systemd, enable and start the service. + // Each step is run separately so failures produce actionable messages. + for _, args := range [][]string{ + {"systemctl", "daemon-reload"}, + {"systemctl", "enable", "brev-sshd.service"}, + {"systemctl", "start", "brev-sshd.service"}, + } { + cmd := exec.Command("sudo", args...) // #nosec G204 + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("running 'sudo %s': %w", strings.Join(args, " "), err) + } } return nil @@ -134,3 +141,86 @@ func UninstallBrevSSHD() error { return nil } + +// AddAllowedUser appends the given username to the AllowUsers directive in the +// brev-managed sshd_config. If AllowUsers does not yet exist, it is created. +// Idempotent: if the user is already listed, this is a no-op. +func AddAllowedUser(username string) error { + return addAllowedUser(brevSSHDConfigPath, username) +} + +func addAllowedUser(configPath, username string) error { + data, err := os.ReadFile(configPath) // #nosec G304 + if err != nil { + return fmt.Errorf("reading sshd_config: %w", err) + } + + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if !strings.HasPrefix(line, "AllowUsers ") { + continue + } + // AllowUsers line exists — check if user is already listed. + users := strings.Fields(line)[1:] + for _, u := range users { + if u == username { + return nil // already allowed + } + } + lines[i] = line + " " + username + return os.WriteFile(configPath, []byte(strings.Join(lines, "\n")), 0o644) + } + + // No AllowUsers line — append one. + lines = append(lines, "AllowUsers "+username) + return os.WriteFile(configPath, []byte(strings.Join(lines, "\n")), 0o644) +} + +// RemoveAllowedUser removes the given username from the AllowUsers directive. +// If removing the user leaves AllowUsers empty, the directive is removed entirely. +// No-op if the user is not listed or AllowUsers does not exist. +func RemoveAllowedUser(username string) error { + return removeAllowedUser(brevSSHDConfigPath, username) +} + +func removeAllowedUser(configPath, username string) error { + data, err := os.ReadFile(configPath) // #nosec G304 + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("reading sshd_config: %w", err) + } + + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if !strings.HasPrefix(line, "AllowUsers ") { + continue + } + users := strings.Fields(line)[1:] + var kept []string + for _, u := range users { + if u != username { + kept = append(kept, u) + } + } + if len(kept) == 0 { + lines = append(lines[:i], lines[i+1:]...) + } else { + lines[i] = "AllowUsers " + strings.Join(kept, " ") + } + return os.WriteFile(configPath, []byte(strings.Join(lines, "\n")), 0o644) + } + + return nil // no AllowUsers line, nothing to do +} + +// ReloadBrevSSHD sends a reload signal to the brev-sshd service so it picks +// up config changes without dropping existing connections. +func ReloadBrevSSHD() error { + cmd := exec.Command("bash", "-c", "sudo systemctl reload brev-sshd.service") // #nosec G204 + if err := cmd.Run(); err != nil { + return fmt.Errorf("reloading brev-sshd: %w", err) + } + return nil +} diff --git a/pkg/cmd/register/sshd_test.go b/pkg/cmd/register/sshd_test.go index 40f2445f..6257d89c 100644 --- a/pkg/cmd/register/sshd_test.go +++ b/pkg/cmd/register/sshd_test.go @@ -1,6 +1,8 @@ package register import ( + "os" + "path/filepath" "strings" "testing" ) @@ -63,3 +65,94 @@ func Test_BrevSSHD_ImplementsInterface(t *testing.T) { // Compile-time check that BrevSSHD satisfies ManagedSSHDaemon. var _ ManagedSSHDaemon = BrevSSHD{} } + +// helper to write a temp sshd_config and return its path. +func writeTempConfig(t *testing.T, content string) string { + t.Helper() + dir := t.TempDir() + p := filepath.Join(dir, "sshd_config") + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatalf("writing temp config: %v", err) + } + return p +} + +func readConfig(t *testing.T, path string) string { + t.Helper() + data, err := os.ReadFile(path) // #nosec G304 + if err != nil { + t.Fatalf("reading config: %v", err) + } + return string(data) +} + +func Test_addAllowedUser_CreatesDirective(t *testing.T) { + p := writeTempConfig(t, "Port 2222\n") + if err := addAllowedUser(p, "alice"); err != nil { + t.Fatalf("addAllowedUser: %v", err) + } + got := readConfig(t, p) + if !strings.Contains(got, "AllowUsers alice") { + t.Errorf("expected AllowUsers alice, got:\n%s", got) + } +} + +func Test_addAllowedUser_AppendsToExisting(t *testing.T) { + p := writeTempConfig(t, "Port 2222\nAllowUsers alice\n") + if err := addAllowedUser(p, "bob"); err != nil { + t.Fatalf("addAllowedUser: %v", err) + } + got := readConfig(t, p) + if !strings.Contains(got, "AllowUsers alice bob") { + t.Errorf("expected AllowUsers alice bob, got:\n%s", got) + } +} + +func Test_addAllowedUser_Idempotent(t *testing.T) { + p := writeTempConfig(t, "Port 2222\nAllowUsers alice\n") + if err := addAllowedUser(p, "alice"); err != nil { + t.Fatalf("addAllowedUser: %v", err) + } + got := readConfig(t, p) + if strings.Count(got, "alice") != 1 { + t.Errorf("expected exactly one 'alice', got:\n%s", got) + } +} + +func Test_removeAllowedUser_RemovesUser(t *testing.T) { + p := writeTempConfig(t, "Port 2222\nAllowUsers alice bob\n") + if err := removeAllowedUser(p, "alice"); err != nil { + t.Fatalf("removeAllowedUser: %v", err) + } + got := readConfig(t, p) + if !strings.Contains(got, "AllowUsers bob") { + t.Errorf("expected AllowUsers bob, got:\n%s", got) + } + if strings.Contains(got, "alice") { + t.Errorf("alice should have been removed, got:\n%s", got) + } +} + +func Test_removeAllowedUser_RemovesDirectiveWhenEmpty(t *testing.T) { + p := writeTempConfig(t, "Port 2222\nAllowUsers alice\n") + if err := removeAllowedUser(p, "alice"); err != nil { + t.Fatalf("removeAllowedUser: %v", err) + } + got := readConfig(t, p) + if strings.Contains(got, "AllowUsers") { + t.Errorf("AllowUsers directive should be removed, got:\n%s", got) + } +} + +func Test_removeAllowedUser_NoOpWhenMissing(t *testing.T) { + p := writeTempConfig(t, "Port 2222\n") + if err := removeAllowedUser(p, "alice"); err != nil { + t.Fatalf("removeAllowedUser: %v", err) + } +} + +func Test_removeAllowedUser_NoOpWhenFileAbsent(t *testing.T) { + if err := removeAllowedUser("/nonexistent/path/sshd_config", "alice"); err != nil { + t.Fatalf("expected no error for missing file, got: %v", err) + } +} diff --git a/pkg/cmd/register/sshkeys.go b/pkg/cmd/register/sshkeys.go index db36f01b..a29d64ce 100644 --- a/pkg/cmd/register/sshkeys.go +++ b/pkg/cmd/register/sshkeys.go @@ -41,6 +41,18 @@ func GrantSSHAccessToNode( } } + // Add the Linux user to the brev-sshd AllowUsers directive so the + // managed sshd on port 2222 accepts connections for this user. + allowUsersAdded := false + if err := AddAllowedUser(osUser.Username); err != nil { + t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: failed to add user to AllowUsers: %v", err))) + } else { + allowUsersAdded = true + if err := ReloadBrevSSHD(); err != nil { + t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: failed to reload brev-sshd: %v", err))) + } + } + client := nodeClients.NewNodeClient(tokenProvider, config.GlobalConfig.GetBrevPublicAPIURL()) _, err := client.GrantNodeSSHAccess(ctx, connect.NewRequest(&nodev1.GrantNodeSSHAccessRequest{ ExternalNodeId: reg.ExternalNodeID, @@ -53,6 +65,13 @@ func GrantSSHAccessToNode( t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: failed to remove SSH key after failed grant: %v", rerr))) } } + if allowUsersAdded { + if rerr := RemoveAllowedUser(osUser.Username); rerr != nil { + t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: failed to roll back AllowUsers after failed grant: %v", rerr))) + } else { + _ = ReloadBrevSSHD() + } + } return fmt.Errorf("failed to grant SSH access: %w", err) } From 62f5e6f0131a5feb37421346809ec5288b84018b Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 4 Mar 2026 14:58:36 -0800 Subject: [PATCH 3/7] feat(sshd): install/upgrade openssh-server before setting up brev-sshd --- pkg/cmd/register/sshd.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/cmd/register/sshd.go b/pkg/cmd/register/sshd.go index 4af2e840..ec8d4af2 100644 --- a/pkg/cmd/register/sshd.go +++ b/pkg/cmd/register/sshd.go @@ -75,6 +75,11 @@ WantedBy=multi-user.target // 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 { + // Install openssh-server if sshd binary is not present. + if err := ensureSSHDInstalled(); err != nil { + return fmt.Errorf("installing openssh-server: %w", err) + } + // Create config directory if err := os.MkdirAll(brevSSHDDir, 0o755); err != nil { return fmt.Errorf("creating brev-sshd directory: %w", err) @@ -215,6 +220,17 @@ func removeAllowedUser(configPath, username string) error { return nil // no AllowUsers line, nothing to do } +// ensureSSHDInstalled installs openssh-server if missing, or upgrades it if +// a newer version is available. +func ensureSSHDInstalled() error { + cmd := exec.Command("sudo", "apt-get", "install", "-y", "openssh-server") // #nosec G204 + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("apt-get install openssh-server: %w", err) + } + return nil +} + // ReloadBrevSSHD sends a reload signal to the brev-sshd service so it picks // up config changes without dropping existing connections. func ReloadBrevSSHD() error { From 5143357ed954be20b6e452ab914f1e1835ec4440 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 4 Mar 2026 15:26:45 -0800 Subject: [PATCH 4/7] refactor(sshd): move managed sshd installation from register to enable-ssh The managed sshd on port 2222 is only needed when SSH access is granted, not at registration time. This moves the install step into enable-ssh so it runs right before granting access. --- pkg/cmd/enablessh/enablessh.go | 32 +++++++++------------ pkg/cmd/register/register.go | 28 ++++++------------ pkg/cmd/register/register_test.go | 47 ------------------------------- 3 files changed, 22 insertions(+), 85 deletions(-) diff --git a/pkg/cmd/enablessh/enablessh.go b/pkg/cmd/enablessh/enablessh.go index 4809e488..ff55405a 100644 --- a/pkg/cmd/enablessh/enablessh.go +++ b/pkg/cmd/enablessh/enablessh.go @@ -5,7 +5,6 @@ package enablessh import ( "context" "fmt" - "os/exec" "os/user" "github.com/brevdev/brev-cli/pkg/cmd/register" @@ -28,6 +27,7 @@ type EnableSSHStore interface { // can be replaced in tests. type enableSSHDeps struct { platform externalnode.PlatformChecker + sshd register.ManagedSSHDaemon nodeClients externalnode.NodeClientFactory registrationStore register.RegistrationStore } @@ -35,6 +35,7 @@ type enableSSHDeps struct { func defaultEnableSSHDeps(brevHome string) enableSSHDeps { return enableSSHDeps{ platform: register.LinuxPlatform{}, + sshd: register.BrevSSHD{}, nodeClients: register.DefaultNodeClientFactory{}, registrationStore: register.NewFileRegistrationStore(brevHome), } @@ -83,15 +84,15 @@ func runEnableSSH(ctx context.Context, t *terminal.Terminal, s EnableSSHStore, d return breverrors.WrapAndTrace(err) } - return enableSSH(ctx, t, deps.nodeClients, s, reg, brevUser) + return enableSSH(ctx, t, deps, s, reg, brevUser) } // enableSSH grants SSH access to the given node for the current Brev user. -// This is the "reflexive grant" — granting yourself SSH access to the device. +// It ensures the managed sshd is installed before granting access. func enableSSH( ctx context.Context, t *terminal.Terminal, - nodeClients externalnode.NodeClientFactory, + deps enableSSHDeps, tokenProvider externalnode.TokenProvider, reg *register.DeviceRegistration, brevUser *entity.User, @@ -101,8 +102,6 @@ func enableSSH( return fmt.Errorf("failed to determine current Linux user: %w", err) } - checkSSHDaemon(t) - t.Vprint("") t.Vprint(t.Green("Enabling SSH access on this device")) t.Vprint("") @@ -111,22 +110,17 @@ func enableSSH( t.Vprintf(" Linux user: %s\n", u.Username) t.Vprint("") - if err := register.GrantSSHAccessToNode(ctx, t, nodeClients, tokenProvider, reg, brevUser, u); err != nil { + t.Vprint("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("") + + if err := register.GrantSSHAccessToNode(ctx, t, deps.nodeClients, tokenProvider, reg, brevUser, u); err != nil { return fmt.Errorf("enable SSH failed: %w", err) } t.Vprint(t.Green(fmt.Sprintf("SSH access enabled. You can now SSH to this device via: brev shell %s", reg.DisplayName))) return nil } - -// checkSSHDaemon prints a warning if neither "ssh" nor "sshd" systemd services -// appear to be active. It never returns an error — it is best-effort. -func checkSSHDaemon(t *terminal.Terminal) { - for _, svc := range []string{"ssh", "sshd", "brev-sshd"} { - out, err := exec.Command("systemctl", "is-active", svc).Output() //nolint:gosec // fixed service names - if err == nil && len(out) > 0 && string(out[:len(out)-1]) == "active" { - return - } - } - t.Vprintf(" %s\n", t.Yellow("Warning: SSH daemon does not appear to be running. SSH access may not work until sshd is started.")) -} diff --git a/pkg/cmd/register/register.go b/pkg/cmd/register/register.go index 396e49e5..98f69181 100644 --- a/pkg/cmd/register/register.go +++ b/pkg/cmd/register/register.go @@ -64,9 +64,8 @@ type SetupRunner interface { type registerDeps struct { platform externalnode.PlatformChecker prompter terminal.Confirmer - netbird NetBirdManager - sshd ManagedSSHDaemon - setupRunner SetupRunner + netbird NetBirdManager + setupRunner SetupRunner nodeClients externalnode.NodeClientFactory commandRunner CommandRunner fileReader FileReader @@ -77,9 +76,8 @@ func defaultRegisterDeps(brevHome string) registerDeps { return registerDeps{ platform: LinuxPlatform{}, prompter: TerminalPrompter{}, - netbird: Netbird{}, - sshd: BrevSSHD{}, - setupRunner: ShellSetupRunner{}, + netbird: Netbird{}, + setupRunner: ShellSetupRunner{}, nodeClients: DefaultNodeClientFactory{}, commandRunner: ExecCommandRunner{}, fileReader: OSFileReader{}, @@ -155,9 +153,8 @@ 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. Set up managed SSH daemon (port 2222)") - t.Vprint(" 3. Collect hardware profile") - t.Vprint(" 4. Register this machine with Brev") + t.Vprint(" 2. Collect hardware profile") + t.Vprint(" 3. Register this machine with Brev") t.Vprint("") if !deps.prompter.ConfirmYesNo("Proceed with registration?") { @@ -166,21 +163,14 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam } t.Vprint("") - t.Vprint(t.Yellow("[Step 1/4] Setting up Brev tunnel...")) + t.Vprint(t.Yellow("[Step 1/3] 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/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(t.Yellow("[Step 2/3] Collecting hardware profile...")) t.Vprint("") nodeSpec, err := CollectHardwareProfile(deps.commandRunner, deps.fileReader) @@ -192,7 +182,7 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam t.Vprint(FormatNodeSpec(nodeSpec)) t.Vprint("") - t.Vprint(t.Yellow("[Step 4/4] Registering with Brev...")) + t.Vprint(t.Yellow("[Step 3/3] Registering with Brev...")) deviceID := uuid.New().String() client := deps.nodeClients.NewNodeClient(s, config.GlobalConfig.GetBrevPublicAPIURL()) diff --git a/pkg/cmd/register/register_test.go b/pkg/cmd/register/register_test.go index abc8cdbb..1d9db5e8 100644 --- a/pkg/cmd/register/register_test.go +++ b/pkg/cmd/register/register_test.go @@ -80,22 +80,6 @@ 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 @@ -129,7 +113,6 @@ 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{ @@ -437,36 +420,6 @@ 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 { From 36ac4fe67e5811c17962bcc7450fc075cac9c800 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 4 Mar 2026 15:47:15 -0800 Subject: [PATCH 5/7] feat(enable-ssh): explain managed sshd setup before first install When brev-sshd is not yet running, tell the user what will be installed before proceeding. Skip the message on subsequent enable-ssh calls. --- pkg/cmd/enablessh/enablessh.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/enablessh/enablessh.go b/pkg/cmd/enablessh/enablessh.go index ff55405a..c2d1707f 100644 --- a/pkg/cmd/enablessh/enablessh.go +++ b/pkg/cmd/enablessh/enablessh.go @@ -5,7 +5,9 @@ package enablessh import ( "context" "fmt" + "os/exec" "os/user" + "strings" "github.com/brevdev/brev-cli/pkg/cmd/register" "github.com/brevdev/brev-cli/pkg/entity" @@ -110,12 +112,21 @@ func enableSSH( t.Vprintf(" Linux user: %s\n", u.Username) t.Vprint("") - t.Vprint("Setting up managed SSH daemon...") - if err := deps.sshd.Install(); err != nil { - return fmt.Errorf("managed sshd setup failed: %w", err) + if !brevSSHDRunning() { + t.Vprint("This will install and configure a managed SSH daemon on port 2222.") + t.Vprint("It will:") + t.Vprint(" - Install/upgrade openssh-server if needed") + t.Vprint(" - Create a dedicated sshd config at /etc/brev-sshd/sshd_config") + t.Vprint(" - Generate a dedicated ed25519 host key") + t.Vprint(" - Start a systemd service (brev-sshd) listening on port 2222") + t.Vprint("") + t.Vprint("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.Green(" Managed SSH daemon ready (port 2222).")) - t.Vprint("") if err := register.GrantSSHAccessToNode(ctx, t, deps.nodeClients, tokenProvider, reg, brevUser, u); err != nil { return fmt.Errorf("enable SSH failed: %w", err) @@ -124,3 +135,9 @@ func enableSSH( t.Vprint(t.Green(fmt.Sprintf("SSH access enabled. You can now SSH to this device via: brev shell %s", reg.DisplayName))) return nil } + +// brevSSHDRunning returns true if the brev-sshd systemd service is active. +func brevSSHDRunning() bool { + out, err := exec.Command("systemctl", "is-active", "brev-sshd").Output() //nolint:gosec // fixed service name + return err == nil && strings.TrimSpace(string(out)) == "active" +} From 4b28de2e24f6364adf1a4bab0f6dc7dd28532399 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 4 Mar 2026 15:47:41 -0800 Subject: [PATCH 6/7] feat(enable-ssh): simplify sshd setup messaging --- pkg/cmd/enablessh/enablessh.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/enablessh/enablessh.go b/pkg/cmd/enablessh/enablessh.go index c2d1707f..f10c6a08 100644 --- a/pkg/cmd/enablessh/enablessh.go +++ b/pkg/cmd/enablessh/enablessh.go @@ -113,14 +113,10 @@ func enableSSH( t.Vprint("") if !brevSSHDRunning() { - t.Vprint("This will install and configure a managed SSH daemon on port 2222.") - t.Vprint("It will:") - t.Vprint(" - Install/upgrade openssh-server if needed") - t.Vprint(" - Create a dedicated sshd config at /etc/brev-sshd/sshd_config") - t.Vprint(" - Generate a dedicated ed25519 host key") - t.Vprint(" - Start a systemd service (brev-sshd) listening on port 2222") + t.Vprint("This will:") + t.Vprint(" 1. Install or upgrade openssh-server") + t.Vprint(" 2. Set up a secure SSH server on port 2222") t.Vprint("") - t.Vprint("Setting up managed SSH daemon...") if err := deps.sshd.Install(); err != nil { return fmt.Errorf("managed sshd setup failed: %w", err) } From 4f77cc31e1026d3e17a49cb34826c2d0927fc785 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 4 Mar 2026 15:54:21 -0800 Subject: [PATCH 7/7] feat(register): install sshd and explain setup before enabling SSH Tell the user what enabling SSH will do before they accept the prompt. Install the managed sshd before granting access so AllowUsers and the config exist when GrantSSHAccessToNode runs. --- pkg/cmd/register/register.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/cmd/register/register.go b/pkg/cmd/register/register.go index 98f69181..9244464e 100644 --- a/pkg/cmd/register/register.go +++ b/pkg/cmd/register/register.go @@ -213,6 +213,12 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam runSetup(node, t, deps) + t.Vprint("") + t.Vprint("SSH access allows you to connect to this device remotely.") + t.Vprint("This will:") + t.Vprint(" 1. Install or upgrade openssh-server") + t.Vprint(" 2. Set up a secure SSH server on port 2222") + t.Vprint("") if deps.prompter.ConfirmYesNo("Would you like to enable SSH access to this device?") { grantSSHAccess(ctx, t, deps, s, reg, brevUser, osUser) } @@ -345,6 +351,14 @@ func grantSSHAccess(ctx context.Context, t *terminal.Terminal, deps registerDeps t.Vprintf(" Linux user: %s\n", osUser.Username) t.Vprint("") + t.Vprint("Setting up managed SSH daemon...") + if err := InstallBrevSSHD(); err != nil { + t.Vprintf(" Warning: managed sshd setup failed: %v\n", err) + } else { + t.Vprint(t.Green(" Managed SSH daemon ready (port 2222).")) + } + t.Vprint("") + err := GrantSSHAccessToNode(ctx, t, deps.nodeClients, tokenProvider, reg, brevUser, osUser) if err != nil { t.Vprint(" Retrying in 3 seconds...")