Repository: chef/automate
Pull request #7955 (2023-06-07 08:26:17)

SSH User Check API
### :nut_and_bolt: Description: What code changed, and why?
 Added the SSH User Check API for the Bastion as if we hit this api with the required body then it will check If the user entered has the SSH access to the node as well as if password is given does it has the access to run the sudo cammand on that node.
<!-- /!\ Please ensure that you are NOT disclosing any customer information without their consent /!\ -->

### :chains: Related Resources

### :+1: Definition of Done

### :athletic_shoe: How to Build and Test the Change
 - Take the pull of this PR
 - rebuild components/automate-cli/
 - Add the this rebuild to your hab package 
 - Install that cli build on the ubuntu or any linux  supported by Automate
 - Run `chef-automate verify serve`
 - The Required parameter or hitting the request are 
 - ```
    "ip": "ENTER VALID IP ",
    "user_name": "ENTER THE VALID USER NAME",
    "ssh_port": "ENTER THE PORT FOR SSH CONNECTION",
    "private_key": "--- ENTER VALID PRIVATE KEY ----",
    "sudo_password":"ENTER SUDO PASSWORD FOR THE USER"
### :white_check_mark: Checklist

**All PRs** must tick these:

- [ ] I have read the [CONTRIBUTING document](https://github.com/chef/automate/blob/master/CONTRIBUTING.md).
- [ ] All commits signed-off for the [Developer Certification of Origin](https://github.com/chef/chef/blob/master/CONTRIBUTING.md#developer-certification-of-origin-dco).

With occasional exceptions, all PRs **from Progress employees** must tick these:

- [ ] Is the code clear? *(complicated code or lots of comments--subdivide and use well-named methods, meaningful variable names, etc.)*
- [ ] Consistency checked? *(user notifications, user prompts, visual patterns, code patterns, variable names)*
- [ ] Repeated code blocks eliminated? *(adapt and reuse existing components, blocks, functions, etc.)*
- [ ] Spelling, grammar, typos checked? *(at a minimum use `make spell` in any component directory)*
- [ ] Code well-formatted? *(indents, line breaks, etc. improve rather than hinder readability)*

All PRs **from Progress employees** should tick these if appropriate:

- [ ] Tests added/updated? (all new code needs new tests)
- [ ] Docs added/updated? (all customer-facing changes)

*Please add a note next to any checkbox above if you are NOT ticking it.*

### :camera: Screenshots, if applicable
- Success Response
  https://progresssoftware.sharepoint.com/:i:/s/ChefCoreC/EZzC6bAXfGFFlA0D63iCK8sBenBPvp7ZB7Kssvooim69Iw?e=rLfKhK


- If the User Doesn't have the SSH and Password privileges
  https://progresssoftware.sharepoint.com/:i:/s/ChefCoreC/Eb38ar4nFadLhi-EQvMqh4ABEWzlQU1-gflLW3mDJ-rdzw?e=x3lUud

- Demo Video for the SSH User Check API
   https://progresssoftware.sharepoint.com/:v:/s/ChefCoreC/EfQtzyvStuRHvpT9Medjb7cB6b-i2NKFL7pbLVtxqbTSvQ?e=uEUXhz
 
- SSH success from one node to another 
    https://progresssoftware.sharepoint.com/:i:/s/ChefCoreC/EY9A2acc2x5NmQTzL1ScducBhIRShVhGj-hEodrXfU1Qvg?e=mtGlw2

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *chef-automate* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 75f35546b97601c1762f76cc0fa14819722dd331 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/chef-automate/deploys/64803f2c63198a0008555eda |

Patch:
diff --git a/.bldr.toml b/.bldr.toml
index a6f81d5a6..c60a9fcf2 100644
--- a/.bldr.toml
+++ b/.bldr.toml
@@ -457,6 +457,7 @@ paths = [
   "lib/proxy/*",
   "lib/reporting/*",
   "lib/secrets/*",
+  "lib/sshutils/*",
   "lib/stringutils/*",
   "lib/systemresource/*",
   "lib/tls/*",
diff --git a/components/automate-cli/pkg/verifyserver/constants/sshusercheck.go b/components/automate-cli/pkg/verifyserver/constants/sshusercheck.go
new file mode 100644
index 000000000..362929afe
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/constants/sshusercheck.go
@@ -0,0 +1,18 @@
+package constants
+
+const (
+	SUDO_PASSWORD_CMD                           = `echo "%s" | sudo -S ls -l`
+	PEM_FILE_NAME                               = "private_key.pem"
+	SSH_USER_SUCCESS_TITLE                      = "SSH user accessible"
+	SSH_USER_SUCCESS_MESSAGE                    = "SSH user is accessible for the node: "
+	SUDO_PASSWORD_TITLE                         = "Sudo password valid"
+	SUDO_PASSWORD_SUCCESS_MESSAGE               = "SSH user sudo password is valid for the node: "
+	SSH_USER_FAILURE_TITLE                      = "SSH user unaccessible"
+	SSH_USER_ERROR_MESSAGE                      = "SSH user is unaccessible for the node with IP: "
+	SSH_USER_RESOLUTION_MESSAGE                 = "Give SSH access to the user with the give key on the node: "
+	SUDO_PASSWORD_FAILURE_TITLE                 = "Sudo password invalid"
+	SUDO_PASSWORD_ERROR_MESSAGE                 = "SSH user sudo password is invalid for the node with IP: "
+	SUDO_PASSWORD_FAILURE_RESOLUTION_MESSAGE    = "Ensure you have provided the correct sudo password and the user has sudo access on the node: "
+	SUDO_PASSWORD_CONNECTION_ERROR_MESSAGE      = "SSH connection failed on the node so unable to check the sudo password for the node with IP: "
+	SUDO_PASSWORD_CONNECTION_RESOLUTION_MESSAGE = "Ensure the correct credentials are provided for the SSH connection of node with IP: "
+)
diff --git a/components/automate-cli/pkg/verifyserver/models/batchcheck.go b/components/automate-cli/pkg/verifyserver/models/batchcheck.go
index c37ac688d..4e736df13 100644
--- a/components/automate-cli/pkg/verifyserver/models/batchcheck.go
+++ b/components/automate-cli/pkg/verifyserver/models/batchcheck.go
@@ -170,3 +170,25 @@ type CheckAndType struct {
 	CheckType string `json:"check_type"`
 	CheckMsg  string `json:"check_msg"`
 }
+
+type ChecksResponse struct {
+	Passed bool     `json:"passed"`
+	Checks []Checks `json:"checks"`
+}
+
+func NewSuccessCheck(title string, successMsg string) *Checks {
+	return &Checks{
+		Title:         title,
+		Passed:        true,
+		SuccessMsg:    successMsg,
+	}
+}
+
+func NewFailureCheck(title string, errorMsg string, resolutionMsg string) *Checks {
+	return &Checks{
+		Title:         title,
+		Passed:        false,
+		ErrorMsg:      errorMsg,
+		ResolutionMsg: resolutionMsg,
+	}
+}
\ No newline at end of file
diff --git a/components/automate-cli/pkg/verifyserver/models/s3config.go b/components/automate-cli/pkg/verifyserver/models/s3config.go
index b70ee3746..085827e96 100644
--- a/components/automate-cli/pkg/verifyserver/models/s3config.go
+++ b/components/automate-cli/pkg/verifyserver/models/s3config.go
@@ -7,7 +7,7 @@ type S3ConfigRequest struct {
 	BasePath   string `json:"base_path"`
 	AccessKey  string `json:"access_key"`
 	SecretKey  string `json:"secret_key"`
-	Region     string `json:"region`
+	Region     string `json:"region"`
 }
 type S3ConfigResponse struct {
 	Passed bool             `json:"passed"`
diff --git a/components/automate-cli/pkg/verifyserver/models/sshuserschecks.go b/components/automate-cli/pkg/verifyserver/models/sshuserschecks.go
new file mode 100644
index 000000000..807e4de15
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/models/sshuserschecks.go
@@ -0,0 +1,10 @@
+package models
+
+type SshUserChecksRequest struct {
+	Ip           string `json:"ip"`
+	UserName     string `json:"user_name"`
+	Port         string `json:"ssh_port"`
+	PrivateKey   string `json:"private_key"`
+	SudoPassword string `json:"sudo_password"`
+}
+
diff --git a/components/automate-cli/pkg/verifyserver/server/api/v1/handler.go b/components/automate-cli/pkg/verifyserver/server/api/v1/handler.go
index f54219ca2..e6faf7904 100644
--- a/components/automate-cli/pkg/verifyserver/server/api/v1/handler.go
+++ b/components/automate-cli/pkg/verifyserver/server/api/v1/handler.go
@@ -13,6 +13,7 @@ import (
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/portreachableservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/s3configservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/softwareversionservice"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/sshusercheckservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/startmockserverservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/statusservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/stopmockserverservice"
@@ -40,6 +41,7 @@ type Handler struct {
 	FqdnService                  fqdnservice.IFqdnService
 	FirewallService              firewallservice.IFirewallService
 	ValidateCertificateService   certificatevalidation.IValidateCertificateService
+	SshUserCheckService          sshusercheckservice.SshUserCheckService
 }
 
 func NewHandler(logger logger.Logger) *Handler {
@@ -129,3 +131,8 @@ func (h *Handler) AddCertificateValidation(vc certificatevalidation.IValidateCer
 	h.ValidateCertificateService = vc
 	return h
 }
+
+func (h *Handler) AddSshUserCheckService(ssu sshusercheckservice.SshUserCheckService) *Handler {
+	h.SshUserCheckService = ssu
+	return h
+}
diff --git a/components/automate-cli/pkg/verifyserver/server/api/v1/sshusercheck.go b/components/automate-cli/pkg/verifyserver/server/api/v1/sshusercheck.go
new file mode 100644
index 000000000..f35737daa
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/server/api/v1/sshusercheck.go
@@ -0,0 +1,29 @@
+package v1
+
+import (
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/models"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/response"
+	"github.com/gofiber/fiber/v2"
+)
+
+func (h *Handler) CheckSshUser(c *fiber.Ctx) error {
+	req := new(models.SshUserChecksRequest)
+	if err := c.BodyParser(&req); err != nil {
+		h.Logger.Error("Ssh User Check request body parsing failed: ", err)
+		return fiber.NewError(fiber.StatusBadRequest, err.Error())
+	}
+
+	if len(req.Ip) == 0 ||
+		len(req.UserName) == 0 ||
+		len(req.PrivateKey) == 0 ||
+		len(req.Port) == 0 {
+		return fiber.NewError(fiber.StatusBadRequest, `Request Parameters that is 'ip', 'user_name', 'ssh_port' and 'private_key' cannot be empty`)
+	}
+
+	service, err := h.SshUserCheckService.CheckSshUserDetails(req)
+	if err != nil {
+		h.Logger.Error("Error while checking the SSH details: ", err)
+		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
+	}
+	return c.JSON(response.BuildSuccessResponse(service))
+}
diff --git a/components/automate-cli/pkg/verifyserver/server/routes.go b/components/automate-cli/pkg/verifyserver/server/routes.go
index 4c3e7ed76..63c1fd1b0 100644
--- a/components/automate-cli/pkg/verifyserver/server/routes.go
+++ b/components/automate-cli/pkg/verifyserver/server/routes.go
@@ -20,7 +20,7 @@ func (vs *VerifyServer) SetupRoutes() {
 	apiChecksGroup.Post("/port-reachable", vs.Handler.PortReachable)
 	apiChecksGroup.Post("/external-postgresql", vs.Handler.CheckExternalPostgresql)
 	apiChecksGroup.Post("/certificate", vs.Handler.ValidateCertificate)
-
+	apiChecksGroup.Post("/ssh-users", vs.Handler.CheckSshUser)
 	apiChecksGroup.Get("/system-user", vs.Handler.CheckSystemUser)
 	apiChecksGroup.Get("/system-resource", vs.Handler.GetSystemResource)
 	apiChecksGroup.Post("/external-opensearch", vs.Handler.ExternalOpensearch)
diff --git a/components/automate-cli/pkg/verifyserver/server/server.go b/components/automate-cli/pkg/verifyserver/server/server.go
index 831b291c3..af8275e30 100644
--- a/components/automate-cli/pkg/verifyserver/server/server.go
+++ b/components/automate-cli/pkg/verifyserver/server/server.go
@@ -34,6 +34,7 @@ import (
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/portreachableservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/s3configservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/softwareversionservice"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/sshusercheckservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/startmockserverservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/statusservice"
 	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/stopmockserverservice"
@@ -45,6 +46,7 @@ import (
 	"github.com/chef/automate/lib/executil"
 	"github.com/chef/automate/lib/io/fileutils"
 	"github.com/chef/automate/lib/logger"
+	"github.com/chef/automate/lib/sshutils"
 	"github.com/chef/automate/lib/systemresource"
 	"github.com/chef/automate/lib/userutils"
 	"github.com/gofiber/fiber/v2"
@@ -122,8 +124,8 @@ func NewVerifyServer(port string, debug bool) (*VerifyServer, error) {
 		AddExternalOpensearchService(externalopensearchservice.NewExternalOpensearchService(l, constants.TIMEOUT)).
 		AddFqdnService(fqdnservice.NewFqdnService(l, constants.TIMEOUT)).
 		AddFirewallService(firewallservice.NewFirewallService(l, constants.TIMEOUT, port)).
-		AddCertificateValidation(certificatevalidation.NewValidateCertificateService(l))
-
+		AddCertificateValidation(certificatevalidation.NewValidateCertificateService(l)).
+		AddSshUserCheckService(sshusercheckservice.NewSshUserCheckService(l, fileutils.NewFileSystemUtils(), sshutils.NewSSHUtil(sshutils.NewSshClient(), l)))
 	vs := &VerifyServer{
 		Port:    port,
 		Log:     l,
diff --git a/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/mocksshusercheck.go b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/mocksshusercheck.go
new file mode 100644
index 000000000..bc6287173
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/mocksshusercheck.go
@@ -0,0 +1,11 @@
+package sshusercheckservice
+
+import "github.com/chef/automate/components/automate-cli/pkg/verifyserver/models"
+
+type MockSshUserCheckService struct {
+	CheckSshUserDetailsFunc func(*models.SshUserChecksRequest) (*models.ChecksResponse, error)
+}
+
+func (mssu *MockSshUserCheckService) CheckSshUserDetails(req *models.SshUserChecksRequest) (*models.ChecksResponse, error) {
+	return mssu.CheckSshUserDetailsFunc(req)
+}
diff --git a/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/sshusercheckservice.go b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/sshusercheckservice.go
new file mode 100644
index 000000000..4577867db
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/sshusercheckservice.go
@@ -0,0 +1,76 @@
+package sshusercheckservice
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/models"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/constants"
+	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/logger"
+	"github.com/chef/automate/lib/sshutils"
+)
+
+type SshUserCheckService interface {
+	CheckSshUserDetails(*models.SshUserChecksRequest) (*models.ChecksResponse, error)
+}
+
+type SshUserServiceImpl struct {
+	logger      logger.Logger
+	SshUtil     sshutils.SSHUtil
+	pemFileName string
+	FileUtils   fileutils.FileUtils
+}
+
+func NewSshUserCheckService(logger logger.Logger, fileutils fileutils.FileUtils, sshUtils sshutils.SSHUtil) *SshUserServiceImpl {
+	return &SshUserServiceImpl{
+		logger:      logger,
+		SshUtil:     sshUtils,
+		pemFileName: constants.PEM_FILE_NAME,
+		FileUtils:   fileutils,
+	}
+}
+
+func (ssu *SshUserServiceImpl) CheckSshUserDetails(req *models.SshUserChecksRequest) (*models.ChecksResponse, error) {
+	ssu.logger.Debugf("The Request value entered by the user ip: %v, port: %v, userName: %v ", req.Ip, req.Port, req.UserName)
+
+	filePath, err := ssu.FileUtils.CreateTempFile(req.PrivateKey, constants.PEM_FILE_NAME)
+	if err != nil {
+		ssu.logger.Error("Error while creating the key file on the desired file path: ", err)
+		return nil, err
+	}
+
+	sshConfig := sshutils.NewSshConfig(req.Ip, req.Port, filePath, req.UserName)
+
+	sshCheckResponse, isCheckPassed := ssu.CheckSshConnection(sshConfig, req.Ip, req.SudoPassword)
+	serviceResponse := &models.ChecksResponse{}
+	serviceResponse.Passed = isCheckPassed
+	serviceResponse.Checks = sshCheckResponse
+
+	ssu.FileUtils.DeleteTempFile(filePath)
+	return serviceResponse, nil
+}
+
+func (ssu *SshUserServiceImpl) CheckSshConnection(sshConfig sshutils.SSHConfig, ip string, sudoPassword string) ([]models.Checks, bool) {
+	responseArray := []models.Checks{}
+	command := fmt.Sprintf(constants.SUDO_PASSWORD_CMD, sudoPassword)
+	checkResponse, err := ssu.SshUtil.Execute(sshConfig, command)
+	ssu.logger.Debug("The response after creating connection and executing command on the terminal: ", checkResponse)
+	if err != nil {
+		if strings.Contains(checkResponse, "Connection creation falied") {
+			sshCheckResponse := models.NewFailureCheck(constants.SSH_USER_FAILURE_TITLE, constants.SSH_USER_ERROR_MESSAGE+ip, constants.SSH_USER_RESOLUTION_MESSAGE+ip)
+			sudoPasswordCheckResponse := models.NewFailureCheck(constants.SUDO_PASSWORD_FAILURE_TITLE, constants.SUDO_PASSWORD_CONNECTION_ERROR_MESSAGE+ip, constants.SUDO_PASSWORD_CONNECTION_RESOLUTION_MESSAGE+ip)
+			responseArray = append(responseArray, *sshCheckResponse, *sudoPasswordCheckResponse)
+			return responseArray, false
+		}
+		sshCheckResponse := models.NewSuccessCheck(constants.SSH_USER_SUCCESS_TITLE, constants.SSH_USER_SUCCESS_MESSAGE+ip)
+		sudoPasswordCheckResponse := models.NewFailureCheck(constants.SUDO_PASSWORD_FAILURE_TITLE, constants.SUDO_PASSWORD_ERROR_MESSAGE+ip, constants.SUDO_PASSWORD_FAILURE_RESOLUTION_MESSAGE+ip)
+		responseArray = append(responseArray, *sshCheckResponse, *sudoPasswordCheckResponse)
+		return responseArray, false
+	}
+	sshCheckResponse := models.NewSuccessCheck(constants.SSH_USER_SUCCESS_TITLE, constants.SSH_USER_SUCCESS_MESSAGE+ip)
+	sudoPasswordCheckResponse := models.NewSuccessCheck(constants.SUDO_PASSWORD_TITLE, constants.SUDO_PASSWORD_SUCCESS_MESSAGE+ip)
+	responseArray = append(responseArray, *sshCheckResponse, *sudoPasswordCheckResponse)
+	return responseArray, true
+
+}
diff --git a/lib/sshutils/mocksshclient.go b/lib/sshutils/mocksshclient.go
new file mode 100644
index 000000000..5960c769e
--- /dev/null
+++ b/lib/sshutils/mocksshclient.go
@@ -0,0 +1,46 @@
+package sshutils
+
+import "golang.org/x/crypto/ssh"
+
+type MockSshClient struct {
+	Dialfunc            func(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error)
+	NewSessionfunc      func(*ssh.Client) (*ssh.Session, error)
+	ParsePrivateKeyfunc func(pemBytes []byte) (ssh.Signer, error)
+	PublicKeyfunc       func(signers ssh.Signer) ssh.AuthMethod
+	CombinedOutputfunc  func(cmd string, session *ssh.Session) ([]byte, error)
+	Closefunc           func(*ssh.Session) error
+	Normalizefunc       func(address string) string
+	Newfunc             func(files ...string) (ssh.HostKeyCallback, error)
+}
+
+func (mssh *MockSshClient) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+	return mssh.Dialfunc(network, addr, config)
+}
+
+func (mssh *MockSshClient) NewSession(client *ssh.Client) (*ssh.Session, error) {
+	return mssh.NewSessionfunc(client)
+}
+
+func (mssh *MockSshClient) ParsePrivateKey(pemBytes []byte) (ssh.Signer, error) {
+	return mssh.ParsePrivateKeyfunc(pemBytes)
+}
+
+func (mssh *MockSshClient) PublicKey(signers ssh.Signer) ssh.AuthMethod {
+	return mssh.PublicKeyfunc(signers)
+}
+
+func (mssh *MockSshClient) CombinedOutput(cmd string, session *ssh.Session) ([]byte, error) {
+	return mssh.CombinedOutputfunc(cmd, session)
+}
+
+func (mssh *MockSshClient) Close(session *ssh.Session) error {
+	return mssh.Closefunc(session)
+}
+
+func (mssh *MockSshClient) Normalize(address string) string {
+	return mssh.Normalizefunc(address)
+}
+
+func (mssh *MockSshClient) New(files ...string) (ssh.HostKeyCallback, error) {
+	return mssh.Newfunc(files...)
+}
\ No newline at end of file
diff --git a/lib/sshutils/mocksshutils.go b/lib/sshutils/mocksshutils.go
new file mode 100644
index 000000000..9a1a6d2e5
--- /dev/null
+++ b/lib/sshutils/mocksshutils.go
@@ -0,0 +1,10 @@
+package sshutils
+
+type MockSSHUtilsImpl struct {
+	Executefunc      func(sshConfig SSHConfig, cmd string) (string, error)
+}
+
+func (mssh *MockSSHUtilsImpl) Execute(sshConfig SSHConfig, cmd string) (string, error) {
+	return mssh.Executefunc(sshConfig, cmd)
+}
+
diff --git a/lib/sshutils/sshclient.go b/lib/sshutils/sshclient.go
new file mode 100644
index 000000000..9578a0ed7
--- /dev/null
+++ b/lib/sshutils/sshclient.go
@@ -0,0 +1,55 @@
+package sshutils
+
+import (
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/crypto/ssh/knownhosts"
+)
+
+type ISshClient interface {
+	Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error)
+	NewSession(*ssh.Client) (*ssh.Session, error)
+	ParsePrivateKey(pemBytes []byte) (ssh.Signer, error)
+	PublicKey(signers ssh.Signer) ssh.AuthMethod
+	CombinedOutput(cmd string, session *ssh.Session) ([]byte, error)
+	Close(*ssh.Session) error
+	Normalize(address string) string
+	New(files ...string) (ssh.HostKeyCallback, error)
+}
+
+type SshClient struct{}
+
+func NewSshClient() ISshClient {
+	return &SshClient{}
+}
+
+func (sc *SshClient) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+	return ssh.Dial(network, addr, config)
+}
+
+func (sc *SshClient) NewSession(conn *ssh.Client) (*ssh.Session, error) {
+	return conn.NewSession()
+}
+
+func (sc *SshClient) ParsePrivateKey(pemBytes []byte) (ssh.Signer, error) {
+	return ssh.ParsePrivateKey(pemBytes)
+}
+
+func (sc *SshClient) PublicKey(signers ssh.Signer) ssh.AuthMethod {
+	return ssh.PublicKeys(signers)
+}
+
+func (sc *SshClient) CombinedOutput(cmd string, session *ssh.Session) ([]byte, error) {
+	return session.CombinedOutput(cmd)
+}
+
+func (sc *SshClient) Close(session *ssh.Session) error {
+	return session.Close()
+}
+
+func (sc *SshClient) Normalize(address string) string {
+	return knownhosts.Normalize(address)
+}
+
+func (sc *SshClient) New(files ...string) (ssh.HostKeyCallback, error) {
+	return knownhosts.New(files...)
+}
diff --git a/lib/sshutils/sshutils.go b/lib/sshutils/sshutils.go
new file mode 100644
index 000000000..6ef9bd541
--- /dev/null
+++ b/lib/sshutils/sshutils.go
@@ -0,0 +1,211 @@
+package sshutils
+
+import (
+	"net"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/chef/automate/lib/logger"
+	"github.com/pkg/errors"
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/crypto/ssh/knownhosts"
+)
+
+type SSHConfig struct {
+	SshUser    string
+	SshPort    string
+	SshKeyFile string
+	HostIP     string
+	Timeout    int
+}
+
+type SSHUtilImpl struct {
+	SshClient ISshClient
+	logger    logger.Logger
+}
+
+type SSHUtil interface {
+	Execute(sshConfig SSHConfig, cmd string) (string, error)
+}
+
+func NewSSHUtil(sshclient ISshClient, logger logger.Logger) *SSHUtilImpl {
+	return &SSHUtilImpl{
+		SshClient: sshclient,
+		logger:    logger,
+	}
+}
+
+func NewSshConfigWithTimeout(ip string, port string, keyFile string, userName string, timeout int) SSHConfig {
+	return SSHConfig{
+		SshUser:    userName,
+		SshPort:    port,
+		SshKeyFile: keyFile,
+		HostIP:     ip,
+		Timeout:    timeout,
+	}
+}
+
+func NewSshConfig(ip string, port string, keyFile string, userName string) SSHConfig {
+	return SSHConfig{
+		SshUser:    userName,
+		SshPort:    port,
+		SshKeyFile: keyFile,
+		HostIP:     ip,
+	}
+}
+
+func (s *SSHUtilImpl) GetConnection(sshConfig SSHConfig) (*ssh.Client, error) {
+	config, err := s.GetClientConfig(sshConfig)
+	if err != nil {
+		s.logger.Error("Error while generating the client config:", err)
+		return nil, err
+	}
+	// Open connection
+	addr := sshConfig.HostIP
+	if len(strings.TrimSpace(sshConfig.SshPort)) > 0 {
+		addr = addr + ":" + sshConfig.SshPort
+	} else {
+		addr = addr + ":22"
+	}
+	conn, err := s.SshClient.Dial("tcp", addr, config)
+	if conn == nil || err != nil {
+		s.logger.Error("dial failed:%v\n", err)
+		return nil, err
+	}
+	return conn, err
+}
+
+func (s *SSHUtilImpl) GetClientConfig(sshConfig SSHConfig) (*ssh.ClientConfig, error) {
+	pemBytes, err := os.ReadFile(sshConfig.SshKeyFile)
+	if err != nil {
+		s.logger.Error("Unable to read private key: %v", err)
+		return nil, err
+	}
+	signer, err := s.SshClient.ParsePrivateKey(pemBytes)
+	if err != nil {
+		s.logger.Errorf("Parsing key failed: %v", err)
+		return nil, err
+	}
+	// Client config
+	return &ssh.ClientConfig{
+		User:            sshConfig.SshUser,
+		Auth:            []ssh.AuthMethod{s.SshClient.PublicKey(signer)},
+		HostKeyCallback: s.HostKeyCallback,
+	}, nil
+}
+
+func (s *SSHUtilImpl) HostKeyCallback(host string, remote net.Addr, pubkey ssh.PublicKey) error {
+	var keyErr *knownhosts.KeyError
+	kh, err := s.CheckKnownHosts()
+	if err != nil {
+		s.logger.Error("Error while getting the list of knows Host:", err)
+		return err
+	}
+	hErr := kh(host, remote, pubkey)
+	// Reference: https://blog.golang.org/go1.13-errors
+	// To understand what errors.As is.
+	if errors.As(hErr, &keyErr) && len(keyErr.Want) > 0 {
+		// Reference: https://www.godoc.org/golang.org/x/crypto/ssh/knownhosts#KeyError
+		// if keyErr.Want slice is empty then host is unknown, if keyErr.Want is not empty
+		// and if host is known then there is key mismatch the connection is then rejected.
+		s.logger.Printf("WARNING: Given hostkeystring is not a key of %s, either a MiTM attack or %s has reconfigured the host pub key.", host, host)
+		return keyErr
+	} else if errors.As(hErr, &keyErr) && len(keyErr.Want) == 0 {
+		// host key not found in known_hosts then give a warning and continue to connect.
+		return s.AddHostKey(host, remote, pubkey)
+	}
+	return nil
+}
+
+func (s *SSHUtilImpl) Execute(sshConfig SSHConfig, cmd string) (string, error) {
+	//Setting default timeout
+	if sshConfig.Timeout == 0 {
+		sshConfig.Timeout = 150
+	}
+	conn, err := s.GetConnection(sshConfig)
+	if err != nil {
+		return "Connection creation failed", err
+	}
+	session, err := s.SshClient.NewSession(conn)
+	if err != nil {
+		s.logger.Error("Session creation failed:", err)
+		return "Session creation failed", err
+	}
+	var output string
+	errCh := make(chan error)
+	go func() {
+		outputByte, err := s.SshClient.CombinedOutput(cmd, session)
+		output = string(outputByte)
+
+		if strings.Contains(output, "Sorry, try again.") || strings.Contains(output, "sudo: a password is required") {
+			errCh <- errors.New("sudo password is incorrect")
+			return
+		}
+		pattern := regexp.MustCompile(`^\[sudo\] password for .+: `)
+		output = pattern.ReplaceAllString(output, "")
+		// if sudo password is correct then replace password prompt with empty string from output
+
+		if err != nil {
+			if strings.Contains(output, "sudo: no tty present and no askpass program specified") {
+				errCh <- errors.New("The sudo password is missing. Make sure to provide sudo_password as enviroment variable and pass -E option while running command.")
+				return
+			}
+			errCh <- err
+			return
+		}
+		errCh <- nil
+	}()
+
+	select {
+	case <-time.After(time.Duration(sshConfig.Timeout) * time.Second):
+		return "", errors.New("command timed out")
+	case err := <-errCh:
+		if err != nil {
+			return output, err
+		}
+	}
+	defer s.SshClient.Close(session)
+	return output, nil
+}
+
+func (s *SSHUtilImpl) CreateKnownHosts() error {
+	f, fErr := os.OpenFile(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"), os.O_CREATE, 0600)
+	if fErr != nil {
+		return fErr
+	}
+	f.Close()
+	return nil
+}
+
+func (s *SSHUtilImpl) CheckKnownHosts() (ssh.HostKeyCallback, error) {
+	err := s.CreateKnownHosts()
+	if err != nil {
+		return nil, err
+	}
+	kh, e := s.SshClient.New(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
+	if e != nil {
+		s.logger.Error("Error while getting the known host: %v", e)
+		return nil, e
+	}
+	return kh, nil
+}
+
+func (s *SSHUtilImpl) AddHostKey(host string, remote net.Addr, pubKey ssh.PublicKey) error {
+	// add host key if host is not found in known_hosts, error object is return, if nil then connection proceeds,
+	// if not nil then connection stops.
+	khFilePath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
+
+	f, fErr := os.OpenFile(khFilePath, os.O_APPEND|os.O_WRONLY, 0600)
+	if fErr != nil {
+		s.logger.Error("Error while opening file on the given path:", fErr)
+		return fErr
+	}
+	defer f.Close()
+	knownHosts := s.SshClient.Normalize(remote.String())
+	_, fileErr := f.WriteString(knownhosts.Line([]string{knownHosts}, pubKey))
+	f.WriteString("\n")
+	return fileErr
+}
diff --git a/tools/credscan/credscan.go b/tools/credscan/credscan.go
index 685cc0523..39a713021 100644
--- a/tools/credscan/credscan.go
+++ b/tools/credscan/credscan.go
@@ -147,6 +147,15 @@ var a2Config = config{
 		// Exclude test file for certificatevalidation with uses RSA KEY for test cases
 		{regex: `components/automate-cli/pkg/verifyserver/services/certificatevalidation/certificatevalidation_test.go`},
 
+		// Exclude test file for private key validation which uses RSA KEY for test cases
+		{regex: `components/automate-cli/pkg/verifyserver/services/sshusercheckservice/testfiles/ssh.pem`},
+
+		// Exclude test file for private key validation which uses RSA KEY for test cases
+		{regex: `components/automate-cli/pkg/verifyserver/server/api/v1/sshuserchecks_test.go`},
+
+		//Exclude test file for private key validation with uses RSA KEY for test cases
+		{regex: `lib/sshutils/testfiles/ssh`},
+
 		// Test data for hadeployconfig
 		{regex: `lib/config/testdata/.*\.toml`},
 		{regex: `lib/config/hadeployconfig_test.go`},


Test patch:
diff --git a/components/automate-cli/pkg/verifyserver/server/api/v1/sshuserchecks_test.go b/components/automate-cli/pkg/verifyserver/server/api/v1/sshuserchecks_test.go
new file mode 100644
index 000000000..d149def15
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/server/api/v1/sshuserchecks_test.go
@@ -0,0 +1,235 @@
+package v1_test
+
+import (
+	"io"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/models"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/server"
+	v1 "github.com/chef/automate/components/automate-cli/pkg/verifyserver/server/api/v1"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/sshusercheckservice"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/utils/fiberutils"
+	"github.com/chef/automate/lib/logger"
+	"github.com/gofiber/fiber/v2"
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	SshUserSuccessMessageWithSudoPassword    = `{"status":"SUCCESS","result":{"passed":true,"checks":[{"title":"SSH user accessible","passed":true,"success_msg":"SSH user is accessible for the node: 1.1.1.1","error_msg":"","resolution_msg":""},{"title":"Sudo password valid","passed":true,"success_msg":"SSH user sudo password is valid for the node: 1.1.1.1","error_msg":"","resolution_msg":""}]}}`
+	SshUserSuccessMessageWithoutSudoPassword = `{"status":"SUCCESS","result":{"passed":true,"checks":[{"title":"SSH user accessible","passed":true,"success_msg":"SSH user is accessible for the node: 1.1.1.1","error_msg":"","resolution_msg":""},{"title":"Sudo password valid","passed":true,"success_msg":"SSH user sudo password is valid for the node: 1.1.1.1","error_msg":"","resolution_msg":""}]}}`
+	SshUserFailureMessageWithWrongPassword   = `{"status":"SUCCESS","result":{"passed":false,"checks":[{"title":"SSH user accessible","passed":true,"success_msg":"SSH user is accessible for the node: 1.1.1.1","error_msg":"","resolution_msg":""},{"title":"Sudo password invalid","passed":false,"success_msg":"","error_msg":"SSH user sudo password is invalid for the node with IP 1.1.1.1","resolution_msg":"Ensure you have provided the correct sudo password and the user has sudo access on the node: 1.1.1.1"}]}}`
+	SshUserResponseWithEmptyRequestBody      = `{"status":"FAILED","result":null,"error":{"code":400,"message":"Request Parameters that is 'ip', 'user_name', 'ssh_port' and 'private_key' cannot be empty"}}`
+	SshUserResponseErrorBodyParser           = `{"status":"FAILED","result":null,"error":{"code":400,"message":"invalid character '}' looking for beginning of object key string"}}`
+	SshUserSuccessTitle                      = `SSH user accessible`
+	SshUserSuccessMessage                    = `SSH user is accessible for the node: 1.1.1.1`
+	SudoSuccessTitle                         = `Sudo password valid`
+	SudoCheckSuccessMessage                  = `SSH user sudo password is valid for the node: 1.1.1.1`
+)
+
+func SetupMockSshUserChecksService(responseBody *models.ChecksResponse, err error) sshusercheckservice.SshUserCheckService {
+	return &sshusercheckservice.MockSshUserCheckService{
+		CheckSshUserDetailsFunc: func(sucr *models.SshUserChecksRequest) (*models.ChecksResponse, error) {
+			return responseBody, err
+		},
+	}
+}
+
+func SetUpSshUserCheckHandlers(ssu sshusercheckservice.SshUserCheckService) (*fiber.App, error) {
+	log, _ := logger.NewLogger("text", "debug")
+	fconf := fiber.Config{
+		ServerHeader: server.SERVICE,
+		ErrorHandler: fiberutils.CustomErrorHandler,
+	}
+	app := fiber.New(fconf)
+	handler := v1.NewHandler(log).
+		AddSshUserCheckService(ssu)
+	vs := &server.VerifyServer{
+		Port:    server.DEFAULT_PORT,
+		Log:     log,
+		App:     app,
+		Handler: handler,
+	}
+	vs.SetupRoutes()
+	return vs.App, nil
+}
+
+func TestCheckSshUser(t *testing.T) {
+	tests := []struct {
+		description   string
+		responseBody  *models.ChecksResponse
+		expectedCode  int
+		expectedBody  string
+		expectedError error
+		body          string
+	}{
+		{
+			description: "200:Success status route with sudo password",
+			responseBody: &models.ChecksResponse{
+				Passed: true,
+				Checks: []models.Checks{
+					{
+						Title:         SshUserSuccessTitle,
+						Passed:        true,
+						SuccessMsg:    SshUserSuccessMessage,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+					{
+						Title:         SudoSuccessTitle,
+						Passed:        true,
+						SuccessMsg:    SudoCheckSuccessMessage,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+				},
+			},
+			expectedCode: 200,
+			expectedBody: SshUserSuccessMessageWithSudoPassword,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name": "admin",
+				"ssh_port": "22",
+				"private_key": "----- BEGIN PRIVATE RSA KEY ------",
+				"sudo_password": "adminpass"
+			}`,
+		},
+		{
+			description: "200:Success route without sudopassword being set by the user",
+			responseBody: &models.ChecksResponse{
+				Passed: true,
+				Checks: []models.Checks{
+					{
+						Title:         SshUserSuccessTitle,
+						Passed:        true,
+						SuccessMsg:    SshUserSuccessMessage,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+					{
+						Title:         SudoSuccessTitle,
+						Passed:        true,
+						SuccessMsg:    SudoCheckSuccessMessage,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+				},
+			},
+			expectedCode: 200,
+			expectedBody: SshUserSuccessMessageWithoutSudoPassword,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name": "admin",
+				"ssh_port": "22",
+				"private_key": "----- BEGIN PRIVATE RSA KEY ------"
+			}`,
+		},
+		{
+			description: "200:Success route with sudopassword being set by the user and entered incorrectly",
+			responseBody: &models.ChecksResponse{
+				Passed: false,
+				Checks: []models.Checks{
+					{
+						Title:         SshUserSuccessTitle,
+						Passed:        true,
+						SuccessMsg:    SshUserSuccessMessage,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+					{
+						Title:         "Sudo password invalid",
+						Passed:        false,
+						SuccessMsg:    "",
+						ErrorMsg:      "SSH user sudo password is invalid for the node with IP 1.1.1.1",
+						ResolutionMsg: "Ensure you have provided the correct sudo password and the user has sudo access on the node: 1.1.1.1",
+					},
+				},
+			},
+			expectedCode: 200,
+			expectedBody: SshUserFailureMessageWithWrongPassword,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name": "admin",
+				"ssh_port": "22",
+				"private_key": "----- BEGIN PRIVATE RSA KEY ------",
+				"sudo_password":"12345"
+			}`,
+		},
+		{
+			description:  "400:One of the mandatory field is empty (ip)",
+			responseBody: nil,
+			expectedCode: 400,
+			expectedBody: SshUserResponseWithEmptyRequestBody,
+			body: `{
+				"ip":"",
+				"user_name":"admin",
+				"ssh_port": "22",
+				"private_key":"----- BEGIN PRIVATE RSA KEY ------"
+			}`,
+		},
+		{
+			description:  "400:One of the mandatory field is empty (username)",
+			responseBody: nil,
+			expectedCode: 400,
+			expectedBody: SshUserResponseWithEmptyRequestBody,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name":"",
+				"ssh_port": "22",
+				"private_key":"----- BEGIN PRIVATE RSA KEY ------"
+			}`,
+		},
+		{
+			description:  "400:One of the mandatory field is empty (privateKey)",
+			responseBody: nil,
+			expectedCode: 400,
+			expectedBody: SshUserResponseWithEmptyRequestBody,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name":"",
+				"ssh_port": "22",
+				"private_key":""
+			}`,
+		},
+		{
+			description:  "400:One of the mandatory field is empty (port)",
+			responseBody: nil,
+			expectedCode: 400,
+			expectedBody: SshUserResponseWithEmptyRequestBody,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name":"",
+				"ssh_port": "",
+				"private_key":"----- BEGIN PRIVATE RSA KEY ------"
+			}`,
+		},
+		{
+			description:  "400: Body Parser Error",
+			responseBody: nil,
+			expectedCode: 400,
+			expectedBody: SshUserResponseErrorBodyParser,
+			body: `{
+				"ip":"1.1.1.1",
+				"user_name": "admin",
+			}`,
+		},
+	}
+
+	sshUserCheckEndpoint := "/api/v1/checks/ssh-users"
+
+	for _, test := range tests {
+		t.Run(test.description, func(t *testing.T) {
+			app, _ := SetUpSshUserCheckHandlers(SetupMockSshUserChecksService(test.responseBody, test.expectedError))
+			req := httptest.NewRequest("POST", sshUserCheckEndpoint, strings.NewReader(test.body))
+			req.Header.Add("Content-Type", "application/json")
+			res, err := app.Test(req, -1)
+			assert.NoError(t, err)
+			body, err := io.ReadAll(res.Body)
+			t.Log(body, "body")
+			assert.NoError(t, err, test.description)
+			assert.Contains(t, string(body), test.expectedBody)
+			assert.Equal(t, res.StatusCode, test.expectedCode)
+		})
+	}
+}
diff --git a/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/sshusercheckservice_test.go b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/sshusercheckservice_test.go
new file mode 100644
index 000000000..61aa83171
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/sshusercheckservice_test.go
@@ -0,0 +1,262 @@
+package sshusercheckservice_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/bmizerany/assert"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/models"
+	"github.com/chef/automate/components/automate-cli/pkg/verifyserver/services/sshusercheckservice"
+	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/logger"
+	"github.com/chef/automate/lib/sshutils"
+)
+
+const (
+	testPemFilePath     = "./testfiles/ssh.pem"
+	nodeIp              = "1.1.1.1"
+	userName            = "ubuntu"
+	port                = "22"
+	sudoPassword        = "123456"
+	SuccessTitle        = "SSH user accessible"
+	SuccessSudoPassword = "Sudo password valid"
+	FailureSudoPassword = "Sudo password invalid"
+	FailureSSHUser      = "SSH user unaccessible"
+	SuccessResponse     = "SSH user is accessible for the node: 1.1.1.1"
+	SuccessSudoResponse = "SSH user sudo password is valid for the node: 1.1.1.1"
+)
+
+func TestCheckSshUserDetails(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	ssu := sshusercheckservice.NewSshUserCheckService(log, &fileutils.FileSystemUtils{}, &sshutils.SSHUtilImpl{})
+
+	type args struct {
+		req           *models.SshUserChecksRequest
+		MockFileUtils fileutils.FileUtils
+		MockSSHUtil   sshutils.SSHUtil
+	}
+	tests := []struct {
+		description string
+		args        args
+		want        *models.ChecksResponse
+		wantErr     error
+	}{
+		{
+			description: "SSH Connection and Sudo password is correct",
+			args: args{
+				req: &models.SshUserChecksRequest{
+					Ip:           nodeIp,
+					UserName:     userName,
+					Port:         port,
+					PrivateKey:   testPemFilePath,
+					SudoPassword: sudoPassword,
+				},
+				MockFileUtils: &fileutils.MockFileSystemUtils{
+					CreateTempFileFunc: func(content, filename string) (string, error) {
+						return "", nil
+					},
+					DeleteTempFileFunc: func(tempFile string) error {
+						return nil
+					},
+				},
+				MockSSHUtil: &sshutils.MockSSHUtilsImpl{
+					Executefunc: func(sshConfig sshutils.SSHConfig, cmd string) (string, error) {
+						return "", nil
+					},
+				},
+			},
+			want: &models.ChecksResponse{
+				Passed: true,
+				Checks: []models.Checks{
+					{
+						Title:         SuccessTitle,
+						Passed:        true,
+						SuccessMsg:    SuccessResponse,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+					{
+						Title:         SuccessSudoPassword,
+						Passed:        true,
+						SuccessMsg:    SuccessSudoResponse,
+						ErrorMsg:      "",
+						ResolutionMsg: "",
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			description: "Temp file creation fails",
+			args: args{
+				req: &models.SshUserChecksRequest{
+					Ip:           nodeIp,
+					UserName:     userName,
+					Port:         port,
+					PrivateKey:   testPemFilePath,
+					SudoPassword: sudoPassword,
+				},
+				MockFileUtils: &fileutils.MockFileSystemUtils{
+					CreateTempFileFunc: func(content, filename string) (string, error) {
+						return "", errors.New("file creation failed ")
+					},
+					DeleteTempFileFunc: func(tempFile string) error {
+						return nil
+					},
+				},
+				MockSSHUtil: &sshutils.MockSSHUtilsImpl{
+					Executefunc: func(sshConfig sshutils.SSHConfig, cmd string) (string, error) {
+						return "", nil
+					},
+				},
+			},
+			want:    nil,
+			wantErr: errors.New("file creation failed "),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			ssu.FileUtils = tt.args.MockFileUtils
+			ssu.SshUtil = tt.args.MockSSHUtil
+			got, err := ssu.CheckSshUserDetails(tt.args.req)
+			assert.Equal(t, got, tt.want)
+			assert.Equal(t, err, tt.wantErr)
+		})
+	}
+}
+
+func TestCheckSshConnection(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	ssu := sshusercheckservice.NewSshUserCheckService(log, &fileutils.MockFileSystemUtils{}, &sshutils.SSHUtilImpl{})
+
+	type args struct {
+		SSHConfig    sshutils.SSHConfig
+		ip           string
+		sudoPassword string
+		MockSSHUtil  sshutils.SSHUtil
+	}
+	tests := []struct {
+		description        string
+		args               args
+		want               []models.Checks
+		wantPassedResponse bool
+	}{
+		{
+			description: "The connection and cammand execution passes on the Remote Host",
+			args: args{
+				SSHConfig: sshutils.SSHConfig{
+					SshUser:    "ubuntu",
+					SshPort:    "22",
+					SshKeyFile: testPemFilePath,
+					HostIP:     nodeIp,
+					Timeout:    150,
+				},
+				ip:           nodeIp,
+				sudoPassword: sudoPassword,
+				MockSSHUtil: &sshutils.MockSSHUtilsImpl{
+					Executefunc: func(sshConfig sshutils.SSHConfig, cmd string) (string, error) {
+						return "", nil
+					},
+				},
+			},
+			want: []models.Checks{
+				{
+					Title:         SuccessTitle,
+					Passed:        true,
+					SuccessMsg:    SuccessResponse,
+					ErrorMsg:      "",
+					ResolutionMsg: "",
+				},
+				{
+					Title:         SuccessSudoPassword,
+					Passed:        true,
+					SuccessMsg:    SuccessSudoResponse,
+					ErrorMsg:      "",
+					ResolutionMsg: "",
+				},
+			},
+			wantPassedResponse: true,
+		},
+		{
+			description: "SSH Connection fails",
+			args: args{
+				SSHConfig: sshutils.SSHConfig{
+					SshUser:    "wrongUsername",
+					SshPort:    "22",
+					SshKeyFile: testPemFilePath,
+					HostIP:     nodeIp,
+					Timeout:    150,
+				},
+				ip:           nodeIp,
+				sudoPassword: sudoPassword,
+				MockSSHUtil: &sshutils.MockSSHUtilsImpl{
+					Executefunc: func(sshConfig sshutils.SSHConfig, cmd string) (string, error) {
+						return "Connection creation falied", errors.New("ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain")
+					},
+				},
+			},
+			want: []models.Checks{
+				{
+					Title:         FailureSSHUser,
+					Passed:        false,
+					SuccessMsg:    "",
+					ErrorMsg:      "SSH user is unaccessible for the node with IP: " + nodeIp,
+					ResolutionMsg: "Give SSH access to the user with the give key on the node: " + nodeIp,
+				},
+				{
+					Title:         FailureSudoPassword,
+					Passed:        false,
+					SuccessMsg:    "",
+					ErrorMsg:      "SSH connection failed on the node so unable to check the sudo password for the node with IP: " + nodeIp,
+					ResolutionMsg: "Ensure the correct credentials are provided for the SSH connection of node with IP: " + nodeIp,
+				},
+			},
+			wantPassedResponse: false,
+		},
+		{
+			description: "SSH Connecton was success but the session creation got failed",
+			args: args{
+				SSHConfig: sshutils.SSHConfig{
+					SshUser:    "wrongUsername",
+					SshPort:    "22",
+					SshKeyFile: testPemFilePath,
+					HostIP:     nodeIp,
+					Timeout:    150,
+				},
+				ip:           nodeIp,
+				sudoPassword: sudoPassword,
+				MockSSHUtil: &sshutils.MockSSHUtilsImpl{
+					Executefunc: func(sshConfig sshutils.SSHConfig, cmd string) (string, error) {
+						return "Session creation failed", errors.New("Session creation failed for the remote host")
+					},
+				},
+			},
+			want: []models.Checks{
+				{
+					Title:         SuccessTitle,
+					Passed:        true,
+					SuccessMsg:    SuccessResponse,
+					ErrorMsg:      "",
+					ResolutionMsg: "",
+				},
+				{
+					Title:         FailureSudoPassword,
+					Passed:        false,
+					SuccessMsg:    "",
+					ErrorMsg:      "SSH user sudo password is invalid for the node with IP: " + nodeIp,
+					ResolutionMsg: "Ensure you have provided the correct sudo password and the user has sudo access on the node: " + nodeIp,
+				},
+			},
+			wantPassedResponse: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			ssu.SshUtil = tt.args.MockSSHUtil
+			got, passed := ssu.CheckSshConnection(tt.args.SSHConfig, tt.args.ip, tt.args.sudoPassword)
+			assert.Equal(t, got, tt.want)
+			assert.Equal(t, passed, tt.wantPassedResponse)
+		})
+	}
+}
diff --git a/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/testfiles/ssh.pem b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/testfiles/ssh.pem
new file mode 100644
index 000000000..99d05ca59
--- /dev/null
+++ b/components/automate-cli/pkg/verifyserver/services/sshusercheckservice/testfiles/ssh.pem
@@ -0,0 +1,1 @@
+----- BEGIN PRIVATE RSA KEY ------
\ No newline at end of file
diff --git a/lib/sshutils/sshutils_test.go b/lib/sshutils/sshutils_test.go
new file mode 100644
index 000000000..5dda35797
--- /dev/null
+++ b/lib/sshutils/sshutils_test.go
@@ -0,0 +1,564 @@
+package sshutils_test
+
+import (
+	"errors"
+	"net"
+
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/chef/automate/lib/logger"
+	"github.com/chef/automate/lib/sshutils"
+	"golang.org/x/crypto/ssh"
+)
+
+const (
+	testfile        = `./testfiles/ssh`
+	sudoPassword    = "123456"
+	sudoPasswordCmd = `echo 123456 | sudo -S ls -l`
+	dialFailed      = `dial failed:ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain`
+	nodeIp          = "1.1.1.1"
+	userName        = "ubuntu"
+	port            = "22"
+	timeout         = 150
+)
+
+var sshConfig = sshutils.SSHConfig{
+	SshUser:    "ubuntu",
+	SshPort:    "22",
+	SshKeyFile: testfile,
+	HostIP:     "1.1.1.1",
+	Timeout:    150,
+}
+
+type netAddressTest struct {
+	Address string
+}
+
+func (n *netAddressTest) Network() string {
+	return n.Address
+}
+
+func (n *netAddressTest) String() string {
+	return n.Address
+}
+
+type sshPublicKeyTest struct {
+	key  string
+	data []byte
+}
+
+func (pub *sshPublicKeyTest) Type() string {
+	return pub.key
+}
+
+func (pub *sshPublicKeyTest) Marshal() []byte {
+	return pub.data
+}
+
+func (pub *sshPublicKeyTest) Verify([]byte, *ssh.Signature) error {
+	return nil
+}
+
+func TestGetConnection(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+	type args struct {
+		sshConfig     sshutils.SSHConfig
+		MockSshClient sshutils.ISshClient
+	}
+	tests := []struct {
+		description string
+		args        args
+		wantErr     error
+	}{
+		{
+			description: "If the connection was successfully done",
+			args: args{
+				sshConfig: sshConfig,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return &ssh.Client{}, nil
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			description: "If the connection was not successfully done",
+			args: args{
+				sshConfig: sshConfig,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return nil, errors.New(dialFailed)
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+				},
+			},
+			wantErr: errors.New(dialFailed),
+		},
+		{
+			description: "If the Client Config was not generated as expected",
+			args: args{
+				sshConfig: sshConfig,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return nil, errors.New("Error while generating the client config: Unable to read private key: no such file or directory")
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+				},
+			},
+			wantErr: errors.New("Error while generating the client config: Unable to read private key: no such file or directory"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			sshu.SshClient = tt.args.MockSshClient
+			_, got := sshu.GetConnection(tt.args.sshConfig)
+			assert.Equal(t, got, tt.wantErr)
+		})
+	}
+}
+
+func TestGetClientConfig(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+	type args struct {
+		sshConfig     sshutils.SSHConfig
+		MockSshClient sshutils.ISshClient
+	}
+	tests := []struct {
+		description string
+		args        args
+		wantedErr   error
+	}{
+		{
+			description: "If the client config creation was successful",
+			args: args{
+				sshConfig: sshConfig,
+				MockSshClient: &sshutils.MockSshClient{
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+					Newfunc: func(files ...string) (ssh.HostKeyCallback, error) {
+						return nil, nil
+					},
+					Normalizefunc: func(address string) string {
+						return ""
+					},
+				},
+			},
+			wantedErr: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			sshu.SshClient = tt.args.MockSshClient
+			_, got := sshu.GetClientConfig(tt.args.sshConfig)
+			assert.Equal(t, got, tt.wantedErr)
+		})
+	}
+}
+
+func TestExecute(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+
+	type args struct {
+		sshConfig     sshutils.SSHConfig
+		cmd           string
+		MockSshClient sshutils.ISshClient
+	}
+
+	tests := []struct {
+		description string
+		args        args
+		want        string
+		wantErr     error
+	}{
+		{
+			description: "Connection got eastablish and sudo password pass the check",
+			args: args{
+				sshConfig: sshConfig,
+				cmd:       sudoPasswordCmd,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return &ssh.Client{}, nil
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+					NewSessionfunc: func(c *ssh.Client) (*ssh.Session, error) {
+						return nil, nil
+					},
+					CombinedOutputfunc: func(cmd string, session *ssh.Session) ([]byte, error) {
+						return []byte("total 0"), nil
+					},
+					Closefunc: func(s *ssh.Session) error {
+						return nil
+					},
+				},
+			},
+			want:    "total 0",
+			wantErr: nil,
+		},
+		{
+			description: "SSH Connection fails",
+			args: args{
+				sshConfig: sshConfig,
+				cmd:       sudoPasswordCmd,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return nil, errors.New("dial failed:ssh: handshake failed: ssh: unable to authenticate,attempted methods [none publickey], no supported methods remain")
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+					NewSessionfunc: func(c *ssh.Client) (*ssh.Session, error) {
+						return nil, nil
+					},
+					CombinedOutputfunc: func(cmd string, session *ssh.Session) ([]byte, error) {
+						return nil, nil
+					},
+					Closefunc: func(s *ssh.Session) error {
+						return nil
+					},
+				},
+			},
+			want:    "Connection creation failed",
+			wantErr: errors.New("dial failed:ssh: handshake failed: ssh: unable to authenticate,attempted methods [none publickey], no supported methods remain"),
+		},
+		{
+			description: "SSH Session Creation failed",
+			args: args{
+				sshConfig: sshConfig,
+				cmd:       sudoPasswordCmd,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return &ssh.Client{}, nil
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+					NewSessionfunc: func(c *ssh.Client) (*ssh.Session, error) {
+						return nil, errors.New("Session creation failed")
+					},
+					CombinedOutputfunc: func(cmd string, session *ssh.Session) ([]byte, error) {
+						return nil, nil
+					},
+					Closefunc: func(s *ssh.Session) error {
+						return nil
+					},
+				},
+			},
+			want:    "Session creation failed",
+			wantErr: errors.New("Session creation failed"),
+		},
+		{
+			description: "Combined Output not able to produce the desired output",
+			args: args{
+				sshConfig: sshConfig,
+				cmd:       sudoPassword,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return &ssh.Client{}, nil
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+					NewSessionfunc: func(c *ssh.Client) (*ssh.Session, error) {
+						return nil, nil
+					},
+					CombinedOutputfunc: func(cmd string, session *ssh.Session) ([]byte, error) {
+						return []byte("Sorry try again"), errors.New("Error while executing command on the remote host:Process exited with status 1")
+					},
+					Closefunc: func(s *ssh.Session) error {
+						return nil
+					},
+				},
+			},
+			want:    "Sorry try again",
+			wantErr: errors.New("Error while executing command on the remote host:Process exited with status 1"),
+		},
+		{
+			description: "Combined Ouput not able to execute the command",
+			args: args{
+				sshConfig: sshConfig,
+				cmd:       sudoPasswordCmd,
+				MockSshClient: &sshutils.MockSshClient{
+					Dialfunc: func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+						return &ssh.Client{}, nil
+					},
+					ParsePrivateKeyfunc: func(pemBytes []byte) (ssh.Signer, error) {
+						return nil, nil
+					},
+					PublicKeyfunc: func(signers ssh.Signer) ssh.AuthMethod {
+						return nil
+					},
+					NewSessionfunc: func(c *ssh.Client) (*ssh.Session, error) {
+						return nil, nil
+					},
+					CombinedOutputfunc: func(cmd string, session *ssh.Session) ([]byte, error) {
+						return []byte("sudo: no tty present and no askpass program specified"), errors.New("The sudo password is missing. Make sure to provide sudo_password as enviroment variable and pass -E option while running command.")
+					},
+					Closefunc: func(s *ssh.Session) error {
+						return nil
+					},
+				},
+			},
+			want:    "sudo: no tty present and no askpass program specified",
+			wantErr: errors.New("The sudo password is missing. Make sure to provide sudo_password as enviroment variable and pass -E option while running command."),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			sshu.SshClient = tt.args.MockSshClient
+			got, gotError := sshu.Execute(tt.args.sshConfig, tt.args.cmd)
+			assert.Equal(t, got, tt.want)
+			if gotError != nil {
+				assert.EqualError(t, gotError, tt.wantErr.Error())
+			}
+		})
+	}
+}
+
+func TestCheckKnownHosts(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+	type args struct {
+		MockSshClient sshutils.ISshClient
+	}
+	tests := []struct {
+		description string
+		args        args
+		wantedError error
+	}{
+		{
+			description: "If checking the known Host",
+			args: args{
+				MockSshClient: &sshutils.MockSshClient{
+					Newfunc: func(files ...string) (ssh.HostKeyCallback, error) {
+						return func(host string, remote net.Addr, pubkey ssh.PublicKey) error {
+							return nil
+						}, nil
+					},
+				},
+			},
+			wantedError: nil,
+		},
+		{
+			description: "If the checking in known Host fails",
+			args: args{
+				MockSshClient: &sshutils.MockSshClient{
+					Newfunc: func(files ...string) (ssh.HostKeyCallback, error) {
+						return nil, errors.New("Error while getting the known host: ")
+					},
+				},
+			},
+			wantedError: errors.New("Error while getting the known host: "),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			sshu.SshClient = tt.args.MockSshClient
+			_, gotError := sshu.CheckKnownHosts()
+			assert.Equal(t, gotError, tt.wantedError)
+		})
+	}
+}
+
+func TestCreateKnownHosts(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+	tests := []struct {
+		description string
+		wantedError error
+	}{
+		{
+			description: "If creation of known Host was successfully done",
+			wantedError: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			gotError := sshu.CreateKnownHosts()
+			assert.Equal(t, gotError, tt.wantedError)
+		})
+	}
+}
+
+func TestAddHostKey(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+	type args struct {
+		host          string
+		remote        *netAddressTest
+		pubkey        ssh.PublicKey
+		MockSshClient sshutils.ISshClient
+	}
+	tests := []struct {
+		description string
+		args        args
+		wantErr     error
+	}{
+		{
+			description: "If the Adding host key was successfully done",
+			args: args{
+				host: nodeIp,
+				remote: &netAddressTest{
+					Address: nodeIp,
+				},
+				pubkey: &sshPublicKeyTest{
+					key:  "",
+					data: nil,
+				},
+				MockSshClient: &sshutils.MockSshClient{
+					Normalizefunc: func(address string) string {
+						return ""
+					},
+					Newfunc: func(files ...string) (ssh.HostKeyCallback, error) {
+						return func(host string, remote net.Addr, pubkey ssh.PublicKey) error {
+							return nil
+						}, nil
+					},
+				},
+			},
+			wantErr: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			sshu.SshClient = tt.args.MockSshClient
+			gotError := sshu.AddHostKey(tt.args.host, tt.args.remote, tt.args.pubkey)
+			assert.Equal(t, gotError, tt.wantErr)
+		})
+	}
+}
+
+func TestHostCallKeyBack(t *testing.T) {
+	log, _ := logger.NewLogger("text", "debug")
+	sshu := sshutils.NewSSHUtil(&sshutils.SshClient{}, log)
+	type args struct {
+		host          string
+		remote        *netAddressTest
+		pubkey        ssh.PublicKey
+		MockSshClient sshutils.ISshClient
+	}
+	tests := []struct {
+		description string
+		args        args
+		wantErr     error
+	}{
+		{
+			description: "Client Config creation is a success",
+			args: args{
+				host: nodeIp,
+				remote: &netAddressTest{
+					Address: nodeIp,
+				},
+				pubkey: &sshPublicKeyTest{
+					key:  "",
+					data: nil,
+				},
+				MockSshClient: &sshutils.MockSshClient{
+					Newfunc: func(files ...string) (ssh.HostKeyCallback, error) {
+						return func(host string, remote net.Addr, pubkey ssh.PublicKey) error {
+							return nil
+						}, nil
+					},
+					Normalizefunc: func(address string) string {
+						return ""
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			sshu.SshClient = tt.args.MockSshClient
+			gotError := sshu.HostKeyCallback(tt.args.host, tt.args.remote, tt.args.pubkey)
+			assert.Equal(t, gotError, tt.wantErr)
+		})
+	}
+}
+
+func TestNewSshConfigWithTimeout(t *testing.T) {
+	type args struct {
+		ip       string
+		port     string
+		keyfile  string
+		userName string
+		timeout  int
+	}
+	var arg args = args{
+		ip:       nodeIp,
+		port:     port,
+		keyfile:  testfile,
+		userName: userName,
+		timeout:  timeout,
+	}
+	want := sshutils.SSHConfig{
+		SshUser:    "ubuntu",
+		SshPort:    "22",
+		SshKeyFile: testfile,
+		HostIP:     "1.1.1.1",
+		Timeout:    150,
+	}
+	response := sshutils.NewSshConfigWithTimeout(arg.ip, arg.port, arg.keyfile, arg.userName, arg.timeout)
+	assert.Equal(t, response, want)
+}
+
+func TestNewSshConfig(t *testing.T) {
+	type args struct {
+		ip       string
+		port     string
+		keyfile  string
+		userName string
+	}
+	var arg args = args{
+		ip:       nodeIp,
+		port:     port,
+		keyfile:  testfile,
+		userName: userName,
+	}
+	want := sshutils.SSHConfig{
+		SshUser:    "ubuntu",
+		SshPort:    "22",
+		SshKeyFile: testfile,
+		HostIP:     "1.1.1.1",
+	}
+	response := sshutils.NewSshConfig(arg.ip, arg.port, arg.keyfile, arg.userName)
+	assert.Equal(t, response, want)
+}
\ No newline at end of file
diff --git a/lib/sshutils/testfiles/ssh b/lib/sshutils/testfiles/ssh
new file mode 100644
index 000000000..b8a8ca2a0
--- /dev/null
+++ b/lib/sshutils/testfiles/ssh
@@ -0,0 +1,5 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAkB6J9TGjdwV+TxCKnCouNBvjz+0qjY4Z5mw8B6s23QoTMs4P
+di30qbAOb+a5gb7p+O8zgGUZEydi1/OhjQDjwx6teoEaSIkLd7uvMqzhNMa0KPsD
+At4ZSUVhZURkOm4nnlbLRY6j6j2VPMMwg1YPV1cfWQ4f5d8uKLHO
+-----END RSA PRIVATE KEY-----