diff --git a/api-mocks/__files/logViewer/logs.json b/api-mocks/__files/logViewer/logs.json
new file mode 100644
index 000000000..e5c4e7400
--- /dev/null
+++ b/api-mocks/__files/logViewer/logs.json
@@ -0,0 +1,11 @@
+{
+  "logLines": [
+    "02:49:12 127.0.0.1 GET / 200",
+    "02:49:35 127.0.0.1 GET /index.html 200",
+    "03:01:06 127.0.0.1 GET /images/sponsered.gif 304",
+    "03:52:36 127.0.0.1 GET /search.php 200",
+    "04:17:03 127.0.0.1 GET /admin/style.css 200",
+    "05:04:54 127.0.0.1 GET /favicon.ico 404",
+    "05:38:07 127.0.0.1 GET /js/ads.js 200"
+    ]
+}
\ No newline at end of file
diff --git a/api-mocks/mappings/endpoints-mapping.json b/api-mocks/mappings/endpoints-mapping.json
index 5a3ade401..d98b991d2 100644
--- a/api-mocks/mappings/endpoints-mapping.json
+++ b/api-mocks/mappings/endpoints-mapping.json
@@ -1,5 +1,23 @@
 {
   "mappings": [
+    {
+      "request": {
+        "method": "GET",
+        "url": "/log-viewer",
+        "queryParameters": {
+          "lines": {
+            "matches": "^[0-9]+$"
+          }
+        }
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "Content-Type": "application/json"
+        },
+        "bodyFileName": "logViewer/logs.json"
+      }
+    },
     {
       "request": {
         "method": "GET",
diff --git a/build.gradle.kts b/build.gradle.kts
index 4451a9a0c..61c3cb243 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -55,7 +55,7 @@ allprojects {
 
 tasks {
     named("build") {
-        dependsOn(":cogboard-app:test", ":cogboard-webapp:buildImage")
+        dependsOn(":cogboard-app:test", ":cogboard-webapp:buildImage", ":ssh:buildImage")
     }
     register("cypressInit", Exec::class) {
         setWorkingDir("./functional/cypress-tests")
diff --git a/cogboard-app/build.gradle.kts b/cogboard-app/build.gradle.kts
index f1ce38a03..48a4bd342 100644
--- a/cogboard-app/build.gradle.kts
+++ b/cogboard-app/build.gradle.kts
@@ -23,6 +23,7 @@ dependencies {
     implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.0")
     implementation(kotlin("stdlib-jdk8"))
     implementation("com.jcraft:jsch:0.1.55")
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
 
     testImplementation("org.assertj:assertj-core:3.12.2")
     testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.2")
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt
index 10f6cc433..c7f607334 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt
@@ -40,11 +40,13 @@ class CogboardConstants {
             const val SCHEDULE_DELAY_DEFAULT = 10L // 10 seconds
             const val SSH_TIMEOUT = 5000 // 5000ms -> 5s
             const val SSH_HOST = "sshAddress"
+            const val SSH_PORT = "sshPort"
             const val SSH_KEY = "sshKey"
             const val SSH_KEY_PASSPHRASE = "sshKeyPassphrase"
             const val URL = "url"
-            const val LOG_LINES = "logLines"
-            const val LOG_FILE_PATH = "logFilePath"
+
+            const val LOG_REQUEST_TYPE = "logRequestType"
+            const val LOG_LINES = "logLinesField"
             const val REQUEST_ID = "requestId"
             const val PUBLIC_URL = "publicUrl"
             const val USER = "user"
@@ -84,6 +86,13 @@ class CogboardConstants {
         }
     }
 
+    class ConnectionType {
+        companion object {
+            const val SSH = "SSH"
+            const val HTTP = "HTTP"
+        }
+    }
+
     class RequestMethod {
         companion object {
             const val GET = "get"
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt
index 97609a8c2..bf4712cbe 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt
@@ -31,6 +31,8 @@ class EndpointLoader(
                 this.put(Props.USER, credentials.getString(Props.USER) ?: "")
                 this.put(Props.PASSWORD, credentials.getString(Props.PASSWORD) ?: "")
                 this.put(Props.TOKEN, credentials.getString(Props.TOKEN) ?: "")
+                this.put(Props.SSH_KEY, credentials.getString(Props.SSH_KEY) ?: "")
+                this.put(Props.SSH_KEY_PASSPHRASE, credentials.getString(Props.SSH_KEY_PASSPHRASE) ?: "")
             }
         }
         return this
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt
index b417d92b6..e9288c042 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt
@@ -37,6 +37,8 @@ class CredentialsController : AbstractVerticle() {
 
     private fun JsonObject.filterSensitiveData(): JsonObject {
         this.remove(Props.PASSWORD)
+        this.remove(Props.SSH_KEY)
+        this.remove(Props.SSH_KEY_PASSPHRASE)
         return this
     }
 
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt
index 19cb04715..6727bf401 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt
@@ -5,5 +5,7 @@ data class Credential(
     val label: String,
     val user: String,
     val password: String?,
-    val token: String?
+    val token: String?,
+    val sshKey: String?,
+    val sshKeyPassphrase: String?
 )
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt
index f25b0e32d..1163deb8e 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt
@@ -13,6 +13,10 @@ import io.vertx.core.eventbus.MessageConsumer
 import io.vertx.core.json.JsonObject
 import io.vertx.core.logging.Logger
 import io.vertx.core.logging.LoggerFactory
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import java.io.InputStream
 
 class SSHClient : AbstractVerticle() {
@@ -41,13 +45,14 @@ class SSHClient : AbstractVerticle() {
                 }
     }
 
-    private fun tryToConnect(config: JsonObject) {
-        val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS)
-        try {
-            connect(config)
-        } catch (e: JSchException) {
-            LOGGER.error(e.message)
-            vertx.eventBus().send(eventBusAddress, e)
+    fun tryToConnect(config: JsonObject) {
+        coroutineScope.launch {
+            try {
+                connect(config)
+            } catch (e: JSchException) {
+                val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS)
+                sendError(e, eventBusAddress)
+            }
         }
     }
 
@@ -62,14 +67,17 @@ class SSHClient : AbstractVerticle() {
             initSSHSession(authData)
             if (session.isConnected) {
                 createChannel(createCommand())
+            } else {
+                LOGGER.error("Failed to connect to ${authData.host}")
             }
         }
     }
 
     private fun initSSHSession(authData: SSHAuthData) {
         jsch = JSch()
-        jsch.setKnownHosts("~/.ssh/known_hosts")
-        val session = SessionStrategyFactory(jsch).create(authData).initSession()
+        // jsch.setKnownHosts("~/.ssh/known_hosts")  for security reasons this should be used
+        session = SessionStrategyFactory(jsch).create(authData).initSession()
+        session.setConfig("StrictHostKeyChecking", "no") // not secure
         session.connect(CogboardConstants.Props.SSH_TIMEOUT)
     }
 
@@ -83,14 +91,32 @@ class SSHClient : AbstractVerticle() {
 
     private fun executeCommandAndSendResult(config: JsonObject) {
         val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS)
-        val responseBuffer = Buffer.buffer()
-        responseBuffer.appendBytes(sshInputStream.readAllBytes())
+        val responseBuffer = readResponse()
         vertx.eventBus().send(eventBusAddress, responseBuffer)
         channel.disconnect()
         session.disconnect()
     }
 
+    private fun readResponse(): Buffer {
+        val responseBuffer = Buffer.buffer()
+        val tmpBuf = ByteArray(512)
+        var readBytes = sshInputStream.read(tmpBuf, 0, 512)
+        while (readBytes != -1) {
+            responseBuffer.appendBytes(tmpBuf, 0, readBytes)
+            readBytes = sshInputStream.read(tmpBuf, 0, 512)
+        }
+
+        return responseBuffer
+    }
+
+    private fun sendError(e: Exception, eventBusAddress: String) {
+        LOGGER.error(e.message)
+        vertx.eventBus().send(eventBusAddress, e.message)
+    }
+
     companion object {
         val LOGGER: Logger = LoggerFactory.getLogger(SSHClient::class.java)
+
+        val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
     }
 }
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt
index abca29a42..1dfa64105 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt
@@ -2,6 +2,5 @@ package com.cognifide.cogboard.ssh.auth
 
 enum class AuthenticationType {
     BASIC,
-    TOKEN,
     SSH_KEY
 }
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt
index b2f6690f6..867adcb9a 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt
@@ -1,25 +1,24 @@
 package com.cognifide.cogboard.ssh.auth
 
-import com.cognifide.cogboard.CogboardConstants
+import com.cognifide.cogboard.CogboardConstants.Props
 import com.cognifide.cogboard.ssh.auth.AuthenticationType.BASIC
-import com.cognifide.cogboard.ssh.auth.AuthenticationType.TOKEN
 import com.cognifide.cogboard.ssh.auth.AuthenticationType.SSH_KEY
 import io.vertx.core.json.Json
 import io.vertx.core.json.JsonArray
 import io.vertx.core.json.JsonObject
 
 class SSHAuthData(private val config: JsonObject) {
-    val user = config.getString(CogboardConstants.Props.USER) ?: ""
-    val password = config.getString(CogboardConstants.Props.PASSWORD) ?: ""
-    val token = config.getString(CogboardConstants.Props.TOKEN) ?: ""
-    val key = config.getString(CogboardConstants.Props.SSH_KEY) ?: ""
-    val host = config.getString(CogboardConstants.Props.SSH_HOST) ?: ""
+    val user = config.getString(Props.USER) ?: ""
+    val password = config.getString(Props.PASSWORD) ?: ""
+    val token = config.getString(Props.TOKEN) ?: ""
+    val key = config.getString(Props.SSH_KEY) ?: ""
+    val host = config.getString(Props.SSH_HOST) ?: ""
+    val port = config.getInteger(Props.SSH_PORT) ?: 22
     val authenticationType = fromConfigAuthenticationType()
 
     private fun fromConfigAuthenticationType(): AuthenticationType {
-        val authTypesString = config.getString(CogboardConstants.Props.AUTHENTICATION_TYPES)
-
-        val authTypes = authTypesString?.let { Json.decodeValue(authTypesString) } ?: JsonArray()
+        val authTypes = config.getString(Props.AUTHENTICATION_TYPES)?.let {
+            Json.decodeValue(it) } ?: JsonArray()
 
         return (authTypes as JsonArray)
                 .map { AuthenticationType.valueOf(it.toString()) }
@@ -28,21 +27,19 @@ class SSHAuthData(private val config: JsonObject) {
 
     private fun hasAuthTypeCorrectCredentials(authType: AuthenticationType): Boolean =
             when {
-                authType == TOKEN && user.isNotBlank() && token.isNotBlank() -> true
                 authType == SSH_KEY && key.isNotBlank() -> true
                 else -> authType == BASIC && user.isNotBlank() && password.isNotBlank()
             }
 
     fun getAuthenticationString(): String =
             when (authenticationType) {
-                BASIC -> config.getString(CogboardConstants.Props.PASSWORD)
-                TOKEN -> config.getString(CogboardConstants.Props.TOKEN)
-                SSH_KEY -> config.getString(CogboardConstants.Props.SSH_KEY)
+                BASIC -> config.getString(Props.PASSWORD)
+                SSH_KEY -> config.getString(Props.SSH_KEY)
             }
 
     fun createCommand(): String {
-        val logLines = config.getString(CogboardConstants.Props.LOG_LINES) ?: "0"
-        val logFilePath = config.getString(CogboardConstants.Props.LOG_FILE_PATH) ?: ""
+        val logLines = config.getInteger(Props.LOG_LINES, 0)
+        val logFilePath = config.getString(Props.PATH, "")
 
         return "cat $logFilePath | tail -$logLines"
     }
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt
index 88b54dd2c..cc7df076e 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt
@@ -1,7 +1,6 @@
 package com.cognifide.cogboard.ssh.session
 
 import com.cognifide.cogboard.ssh.auth.AuthenticationType.BASIC
-import com.cognifide.cogboard.ssh.auth.AuthenticationType.TOKEN
 import com.cognifide.cogboard.ssh.auth.AuthenticationType.SSH_KEY
 import com.cognifide.cogboard.ssh.auth.SSHAuthData
 import com.cognifide.cogboard.ssh.session.strategy.BasicAuthSessionStrategy
@@ -12,7 +11,7 @@ import com.jcraft.jsch.JSch
 class SessionStrategyFactory(private val jsch: JSch) {
     fun create(authData: SSHAuthData): SessionStrategy =
             when (authData.authenticationType) {
-                BASIC, TOKEN -> {
+                BASIC -> {
                     BasicAuthSessionStrategy(jsch, authData)
                 }
                 SSH_KEY -> {
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt
index 96920b28e..119d0293a 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt
@@ -7,7 +7,7 @@ import com.jcraft.jsch.Session
 class BasicAuthSessionStrategy(jsch: JSch, authData: SSHAuthData) : SessionStrategy(jsch, authData) {
 
     override fun initSession(): Session {
-        val session = jsch.getSession(authData.user, authData.host)
+        val session = jsch.getSession(authData.user, authData.host, authData.port)
         session.setPassword(securityString)
 
         return session
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt
index 65e7c5b72..38133eeca 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt
@@ -3,15 +3,16 @@ package com.cognifide.cogboard.ssh.session.strategy
 import com.cognifide.cogboard.ssh.auth.SSHAuthData
 import com.jcraft.jsch.JSch
 import com.jcraft.jsch.Session
+import io.netty.util.internal.StringUtil.EMPTY_STRING
 
 class SSHKeyAuthSessionStrategy(jSch: JSch, authData: SSHAuthData) : SessionStrategy(jSch, authData) {
     override fun initSession(): Session {
-        if (authData.password == "") {
+        if (authData.password == EMPTY_STRING) {
             jsch.addIdentity(securityString)
         } else {
             jsch.addIdentity(securityString, authData.password)
         }
-        val session = jsch.getSession(authData.user, authData.host)
+        val session = jsch.getSession(authData.user, authData.host, authData.port)
         session.setConfig("PreferredAuthentications", "publickey")
 
         return session
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/SSHWidget.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/SSHWidget.kt
deleted file mode 100644
index 43939a695..000000000
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/SSHWidget.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.cognifide.cogboard.widget
-
-import com.cognifide.cogboard.CogboardConstants.Props
-import com.cognifide.cogboard.CogboardConstants.Event
-import com.cognifide.cogboard.config.service.BoardsConfigService
-import io.vertx.core.Vertx
-import io.vertx.core.eventbus.MessageConsumer
-import io.vertx.core.json.JsonObject
-import java.nio.Buffer
-
-abstract class SSHWidget(
-    vertx: Vertx,
-    config: JsonObject,
-    serv: BoardsConfigService
-) : AsyncWidget(vertx, config, serv) {
-    val sshKey: String = config.endpointProp(Props.SSH_KEY)
-    val host: String = config.endpointProp(Props.SSH_HOST)
-    val logPath: String = config.endpointProp(Props.LOG_FILE_PATH)
-    val logLines: String = config.endpointProp(Props.LOG_LINES)
-    private lateinit var sshConsumer: MessageConsumer<Buffer>
-
-    fun registerForSSH(eventBusAddress: String) {
-        sshConsumer = vertx.eventBus()
-                .consumer<Buffer>(eventBusAddress)
-                .handler {
-                    handleSSHResponse(it.body())
-                }
-    }
-
-    abstract fun handleSSHResponse(body: Buffer?)
-
-    fun unregisterFromSSH() {
-        if (::sshConsumer.isInitialized) {
-            sshConsumer.unregister()
-        }
-    }
-
-    fun sendRequestForLogs(config: JsonObject) {
-        ensureConfigIsPrepared(config)
-        vertx.eventBus().send(Event.SSH_COMMAND, config)
-    }
-
-    private fun ensureConfigIsPrepared(config: JsonObject) {
-        config.getString(Props.USER) ?: config.put(Props.USER, user)
-        config.getString(Props.PASSWORD) ?: config.put(Props.PASSWORD, password)
-        config.getString(Props.TOKEN) ?: config.put(Props.TOKEN, token)
-        config.getString(Props.SSH_KEY) ?: config.put(Props.SSH_KEY, sshKey)
-        config.getString(Props.SSH_HOST) ?: config.put(Props.SSH_HOST, host)
-        config.getString(Props.LOG_FILE_PATH) ?: config.put(Props.LOG_FILE_PATH, logPath)
-        config.getString(Props.LOG_LINES) ?: config.put(Props.LOG_LINES, logLines)
-    }
-}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt
index e057a29be..ee5235c1b 100644
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt
@@ -20,7 +20,7 @@ import com.cognifide.cogboard.widget.type.WorldClockWidget
 import com.cognifide.cogboard.widget.type.randompicker.RandomPickerWidget
 import com.cognifide.cogboard.widget.type.sonarqube.SonarQubeWidget
 import com.cognifide.cogboard.widget.type.zabbix.ZabbixWidget
-import com.cognifide.cogboard.widget.type.LogViewerWidget
+import com.cognifide.cogboard.widget.type.logviewer.LogViewerWidget
 import io.vertx.core.Vertx
 import io.vertx.core.json.JsonArray
 import io.vertx.core.json.JsonObject
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategy.kt
new file mode 100644
index 000000000..dcafef062
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategy.kt
@@ -0,0 +1,23 @@
+package com.cognifide.cogboard.widget.connectionStrategy
+
+import com.cognifide.cogboard.CogboardConstants
+import com.cognifide.cogboard.http.auth.AuthenticationType
+import io.vertx.core.Vertx
+import io.vertx.core.eventbus.MessageConsumer
+import io.vertx.core.json.JsonObject
+
+abstract class ConnectionStrategy(protected val vertx: Vertx, protected val eventBusAddress: String) {
+    protected fun JsonObject.endpointProp(prop: String): String {
+        return this.getJsonObject(CogboardConstants.Props.ENDPOINT_LOADED)?.getString(prop) ?: ""
+    }
+
+    protected open fun authenticationTypes(): Set<AuthenticationType> {
+        return setOf(AuthenticationType.BASIC)
+    }
+
+    abstract fun sendRequest(address: String, arguments: JsonObject)
+
+    abstract fun getConsumer(eventBusAddress: String): MessageConsumer<*>
+
+    abstract fun handleResponse(response: Any): String
+}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategyFactory.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategyFactory.kt
new file mode 100644
index 000000000..b734cb998
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategyFactory.kt
@@ -0,0 +1,81 @@
+package com.cognifide.cogboard.widget.connectionStrategy
+
+import com.cognifide.cogboard.CogboardConstants.ConnectionType.Companion.HTTP
+import com.cognifide.cogboard.CogboardConstants.ConnectionType.Companion.SSH
+import com.cognifide.cogboard.CogboardConstants.Props
+import io.vertx.core.Vertx
+import io.vertx.core.json.JsonObject
+import java.net.URI
+
+class ConnectionStrategyFactory(
+    config: JsonObject,
+    uri: String
+) {
+    private val connectionType: String
+    private lateinit var vertx: Vertx
+    private lateinit var eventBusAddress: String
+
+    init {
+        connectionType = determineConnectionType(uri, config)
+    }
+
+    fun addVertxInstance(vertx: Vertx): ConnectionStrategyFactory {
+        this.vertx = vertx
+        return this
+    }
+
+    fun addEventBusAddress(eventBusAddress: String): ConnectionStrategyFactory {
+        this.eventBusAddress = eventBusAddress
+        return this
+    }
+
+    private fun determineConnectionType(uri: String, config: JsonObject): String {
+        val url = URI.create(uri)
+        return when (url.scheme) {
+            "http", "https" -> HTTP
+            "ssh" -> {
+                prepareSshConfig(url, config)
+                SSH
+            }
+            else -> {
+                throw UnknownConnectionTypeException("Unknown strategy type")
+            }
+        }
+    }
+
+    private fun prepareSshConfig(uri: URI, config: JsonObject) {
+        config.put(Props.SSH_HOST, uri.host)
+        uri.port.let {
+            if (it != -1) config.put(Props.SSH_PORT, uri.port)
+        }
+    }
+
+    fun checkRequiredParameters() {
+        var message = ""
+        when {
+            !::vertx.isInitialized -> message = "Vertx instance not passed to builder"
+            !::eventBusAddress.isInitialized -> message = "Eventbus address not passed to builder"
+        }
+
+        if (message.isNotBlank()) {
+            throw MissingBuilderParametersException(message)
+        }
+    }
+
+    fun build(): ConnectionStrategy {
+        checkRequiredParameters()
+        return when (connectionType) {
+            HTTP -> HttpConnectionStrategy(vertx, eventBusAddress)
+            SSH -> SSHConnectionStrategy(vertx, eventBusAddress)
+            else -> throw UnknownConnectionTypeException("Unknown strategy type")
+        }
+    }
+}
+
+class UnknownConnectionTypeException(
+    message: String?
+) : RuntimeException(message)
+
+class MissingBuilderParametersException(
+    message: String?
+) : RuntimeException(message)
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/HttpConnectionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/HttpConnectionStrategy.kt
new file mode 100644
index 000000000..c4dc5500a
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/HttpConnectionStrategy.kt
@@ -0,0 +1,52 @@
+package com.cognifide.cogboard.widget.connectionStrategy
+
+import com.cognifide.cogboard.CogboardConstants.Props
+import com.cognifide.cogboard.CogboardConstants.Event
+import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.GET
+import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.PUT
+import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.POST
+import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.DELETE
+import io.vertx.core.Vertx
+import io.vertx.core.eventbus.MessageConsumer
+import io.vertx.core.json.Json
+import io.vertx.core.json.JsonObject
+
+class HttpConnectionStrategy(vertx: Vertx, eventBusAddress: String) :
+        ConnectionStrategy(vertx, eventBusAddress) {
+    override fun sendRequest(address: String, arguments: JsonObject) {
+        when (arguments.getString(Props.LOG_REQUEST_TYPE, "")) {
+            GET -> vertx.eventBus().send(Event.HTTP_GET, getProps(arguments))
+            PUT -> vertx.eventBus().send(Event.HTTP_PUT, putProps(arguments))
+            POST -> vertx.eventBus().send(Event.HTTP_POST, postProps(arguments))
+            DELETE -> vertx.eventBus().send(Event.HTTP_DELETE, basicProps(arguments))
+        }
+    }
+
+    override fun getConsumer(eventBusAddress: String): MessageConsumer<*> =
+        vertx.eventBus().consumer<JsonObject>(eventBusAddress)
+
+    override fun handleResponse(response: Any): String =
+        (response as JsonObject).getString(Props.LOG_LINES)
+
+    private fun basicProps(props: JsonObject): JsonObject =
+         JsonObject()
+             .put(Props.URL, props.endpointProp(Props.URL))
+             .put(Props.EVENT_ADDRESS, eventBusAddress)
+             .put(Props.USER, props.endpointProp(Props.USER))
+             .put(Props.PASSWORD, props.endpointProp(Props.PASSWORD))
+             .put(Props.AUTHENTICATION_TYPES, Json.encode(authenticationTypes()))
+             .put(Props.CONTENT_TYPE, props.endpointProp(Props.CONTENT_TYPE))
+
+    private fun getProps(props: JsonObject): JsonObject =
+        basicProps(props)
+            .put(Props.REQUEST_ID, props.getValue(Props.REQUEST_ID, ""))
+            .put(Props.TOKEN, props.endpointProp(Props.TOKEN))
+
+    private fun putProps(props: JsonObject): JsonObject =
+        basicProps(props)
+            .put(Props.BODY, props.getJsonObject(Props.BODY))
+
+    private fun postProps(props: JsonObject): JsonObject =
+        basicProps(props)
+            .put(Props.BODY, props.getJsonObject(Props.BODY))
+}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/SSHConnectionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/SSHConnectionStrategy.kt
new file mode 100644
index 000000000..bd1805f9d
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/SSHConnectionStrategy.kt
@@ -0,0 +1,41 @@
+package com.cognifide.cogboard.widget.connectionStrategy
+
+import com.cognifide.cogboard.CogboardConstants
+import com.cognifide.cogboard.CogboardConstants.Props
+import io.vertx.core.Vertx
+import io.vertx.core.buffer.Buffer
+import io.vertx.core.eventbus.MessageConsumer
+import io.vertx.core.json.Json
+import io.vertx.core.json.JsonObject
+import java.nio.charset.Charset
+
+open class SSHConnectionStrategy(vertx: Vertx, eventBusAddress: String) :
+        ConnectionStrategy(vertx, eventBusAddress) {
+    override fun sendRequest(address: String, arguments: JsonObject) {
+        val config = prepareConfig(arguments)
+        vertx.eventBus().send(CogboardConstants.Event.SSH_COMMAND, config)
+    }
+
+    override fun getConsumer(eventBusAddress: String): MessageConsumer<*> =
+        vertx.eventBus().consumer<Buffer>(eventBusAddress)
+
+    override fun handleResponse(response: Any): String =
+        (response as Buffer).toString(Charset.defaultCharset())
+
+    private fun prepareConfig(config: JsonObject): JsonObject {
+        val tmpConfig = prepareConfigLines(config = config,
+            Props.USER, Props.PASSWORD, Props.TOKEN, Props.SSH_KEY, Props.SSH_KEY_PASSPHRASE
+        )
+
+        tmpConfig.getString(Props.AUTHENTICATION_TYPES) ?: config.put(Props.AUTHENTICATION_TYPES, Json.encode(authenticationTypes()))
+        tmpConfig.put(Props.EVENT_ADDRESS, eventBusAddress)
+        return tmpConfig
+    }
+
+    private fun prepareConfigLines(config: JsonObject, vararg fields: String): JsonObject {
+        for (field in fields) {
+            config.getString(field) ?: config.put(field, config.endpointProp(field))
+        }
+        return config
+    }
+}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/LogViewerWidget.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/LogViewerWidget.kt
deleted file mode 100644
index efadc4a68..000000000
--- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/LogViewerWidget.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.cognifide.cogboard.widget.type
-
-import com.cognifide.cogboard.config.service.BoardsConfigService
-import com.cognifide.cogboard.widget.BaseWidget
-import io.vertx.core.Vertx
-import io.vertx.core.json.JsonObject
-
-class LogViewerWidget(
-    vertx: Vertx,
-    config: JsonObject,
-    serv: BoardsConfigService
-) : BaseWidget(vertx, config, serv) {
-
-    override fun updateState() {
-        updateStateByCopingPropsToContent(PROPS)
-    }
-
-    companion object {
-        val PROPS = setOf(
-            "endpoint",
-            "schedulePeriod",
-            "path",
-            "logLinesField",
-            "logFileSizeField",
-            "logRecordExpirationField"
-        )
-    }
-}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerWidget.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerWidget.kt
new file mode 100644
index 000000000..aa0c15adb
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerWidget.kt
@@ -0,0 +1,77 @@
+package com.cognifide.cogboard.widget.type.logviewer
+
+import com.cognifide.cogboard.CogboardConstants.Props
+import com.cognifide.cogboard.config.service.BoardsConfigService
+import com.cognifide.cogboard.widget.BaseWidget
+import com.cognifide.cogboard.widget.Widget
+import com.cognifide.cogboard.widget.connectionStrategy.ConnectionStrategy
+import com.cognifide.cogboard.widget.connectionStrategy.ConnectionStrategyFactory
+import com.cognifide.cogboard.widget.type.logviewer.logparser.LogParserStrategy
+import com.cognifide.cogboard.widget.type.logviewer.logparser.LogParserStrategyFactory
+import io.vertx.core.Vertx
+import io.vertx.core.eventbus.Message
+import io.vertx.core.eventbus.MessageConsumer
+import io.vertx.core.json.JsonObject
+
+class LogViewerWidget(
+    vertx: Vertx,
+    config: JsonObject,
+    serv: BoardsConfigService
+) : BaseWidget(vertx, config, serv) {
+    private val address = config.endpointProp(Props.URL)
+    private var consumer: MessageConsumer<*>? = null
+    private val connectionStrategy: ConnectionStrategy = determineConnectionStrategy()
+    private val logParsingStrategy: LogParserStrategy = determineLogParsingStrategy()
+
+    override fun start(): Widget {
+        consumer = connectionStrategy.getConsumer(eventBusAddress)
+        consumer!!.handler {
+            handleResponse(it)
+        }
+        return super.start()
+    }
+
+    override fun stop(): Widget {
+        consumer?.unregister()
+        return super.stop()
+    }
+
+    override fun updateState() {
+        if (address.isNotBlank()) {
+            connectionStrategy.sendRequest(address, config)
+        } else {
+            sendConfigurationError("Endpoint URL is blank")
+        }
+    }
+
+    private fun handleResponse(response: Message<*>) {
+        val responseBody = response.body()
+        if (responseBody is JsonObject) {
+            handleHttpResponse(responseBody)
+        } else {
+            send(prepareLogs(connectionStrategy.handleResponse(responseBody)))
+        }
+    }
+
+    private fun handleHttpResponse(responseBody: JsonObject) {
+        if (checkAuthorized(responseBody)) {
+            send(prepareLogs(connectionStrategy.handleResponse(responseBody)))
+        }
+    }
+
+    private fun prepareLogs(logs: String): JsonObject {
+        var logLines = logs.split("\n")
+        logLines = logLines.filter { it.isNotEmpty() }
+        return JsonObject().put("logs", logParsingStrategy.parseLines(logLines))
+    }
+
+    private fun determineConnectionStrategy() =
+            ConnectionStrategyFactory(config, address)
+                    .addVertxInstance(vertx)
+                    .addEventBusAddress(eventBusAddress)
+                    .build()
+
+    private fun determineLogParsingStrategy() =
+            LogParserStrategyFactory()
+                    .build(LogParserStrategyFactory.MOCK)
+}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategy.kt
new file mode 100644
index 000000000..a5c41303d
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategy.kt
@@ -0,0 +1,17 @@
+package com.cognifide.cogboard.widget.type.logviewer.logparser
+
+import io.vertx.core.json.JsonArray
+import io.vertx.core.json.JsonObject
+
+abstract class LogParserStrategy {
+    fun parseLines(logLines: Collection<String>): JsonArray {
+        val resultArray = JsonArray()
+        for (line in logLines) {
+            val parsedLine = parseLine(line)
+            resultArray.add(parsedLine)
+        }
+        return resultArray
+    }
+
+    abstract fun parseLine(logLine: String): JsonObject
+}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategyFactory.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategyFactory.kt
new file mode 100644
index 000000000..889dcbeb6
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategyFactory.kt
@@ -0,0 +1,16 @@
+package com.cognifide.cogboard.widget.type.logviewer.logparser
+
+class LogParserStrategyFactory {
+    companion object {
+        const val MOCK = "mock"
+    }
+
+    fun build(type: String): LogParserStrategy {
+        return when (type) {
+            MOCK -> MockLogParserStrategy()
+            else -> throw UnknownParserTypeException("Unknown strategy type")
+        }
+    }
+}
+
+class UnknownParserTypeException(message: String) : RuntimeException(message)
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategy.kt
new file mode 100644
index 000000000..4b0d4dd0d
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategy.kt
@@ -0,0 +1,28 @@
+package com.cognifide.cogboard.widget.type.logviewer.logparser
+
+import io.vertx.core.json.JsonObject
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.TYPE
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.DATE
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.PROVIDER
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.MESSAGE
+
+class MockLogParserStrategy : LogParserStrategy() {
+    private val regex = """^(?<$DATE>[0-9-:]+) \*(?<$TYPE>[A-Z]+)\* \[(?<$PROVIDER>[a-zA-Z]+)\][ ]+(?<$MESSAGE>.+)$""".trimMargin().toRegex()
+
+    override fun parseLine(logLine: String): JsonObject {
+        val groups = regex.matchEntire(logLine.trim())?.groups
+
+        return createLogObject(groups)
+    }
+
+    private fun createLogObject(groups: MatchGroupCollection?): JsonObject {
+        val mapOfCapturedValues = mutableMapOf<String, String>()
+        mapOfCapturedValues[TYPE] = groups?.get(TYPE)?.value ?: ""
+        mapOfCapturedValues[DATE] = groups?.get(DATE)?.value ?: ""
+        mapOfCapturedValues[PROVIDER] = groups?.get(PROVIDER)?.value ?: ""
+        mapOfCapturedValues[MESSAGE] = groups?.get(MESSAGE)?.value ?: ""
+
+        val parsedLog = ParsedLog(mapOfCapturedValues)
+        return parsedLog.parsedLogJson
+    }
+}
diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/ParsedLog.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/ParsedLog.kt
new file mode 100644
index 000000000..d9de522f6
--- /dev/null
+++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/ParsedLog.kt
@@ -0,0 +1,63 @@
+package com.cognifide.cogboard.widget.type.logviewer.logparser
+
+import io.vertx.core.json.JsonArray
+import io.vertx.core.json.JsonObject
+
+class ParsedLog(values: Map<String, String>) {
+    companion object {
+        const val DATE = "date"
+        const val TYPE = "type"
+        const val PROVIDER = "Provider"
+        const val MESSAGE = "Message"
+        const val TEMPLATE = "template"
+        const val HEADERS = "headers"
+        const val VARIABLE_DATA = "variableData"
+        const val DESCRIPTION = "description"
+        const val ADDITIONAL_DATA = "additionalData"
+        const val ID = "ID"
+        const val IP_ADDRESS = "IP address"
+        const val PORT = "Port"
+    }
+
+    private val _parsedLogJson = JsonObject()
+    private val variableData = JsonObject()
+
+    val parsedLogJson: JsonObject
+        get() {
+            _parsedLogJson.put(VARIABLE_DATA, variableData)
+            return _parsedLogJson
+        }
+
+    init {
+        values[TYPE]?.let { _parsedLogJson.put(TYPE, it) }
+        values[DATE]?.let { _parsedLogJson.put(DATE, it) }
+        values[PROVIDER]?.let { addFieldToVariableData(PROVIDER, it) }
+        values[MESSAGE]?.let { addFieldToVariableData(MESSAGE, it) }
+        addAdditionalData()
+    }
+
+    private fun addFieldToVariableData(template: String, value: String) {
+        val templateArray = variableData.getJsonArray(TEMPLATE, JsonArray())
+        val headersArray = variableData.getJsonArray(HEADERS, JsonArray())
+        val descriptionArray = variableData.getJsonArray(DESCRIPTION, JsonArray())
+
+        if (!templateArray.contains(template)) {
+            templateArray.add(template)
+            headersArray.add(value)
+            descriptionArray.add("No description")
+        }
+        variableData.put(TEMPLATE, templateArray)
+        variableData.put(HEADERS, headersArray)
+        variableData.put(DESCRIPTION, descriptionArray)
+    }
+
+    private fun addAdditionalData() {
+        val additionalData = JsonObject()
+        additionalData.put(ID, "None")
+        additionalData.put(TYPE.capitalize(), "None")
+        additionalData.put(IP_ADDRESS, "None")
+        additionalData.put(PORT, "None")
+
+        _parsedLogJson.put(ADDITIONAL_DATA, additionalData)
+    }
+}
diff --git a/cogboard-app/src/main/resources/initData/credentials.json b/cogboard-app/src/main/resources/initData/credentials.json
index db175e834..b3e4b8e77 100644
--- a/cogboard-app/src/main/resources/initData/credentials.json
+++ b/cogboard-app/src/main/resources/initData/credentials.json
@@ -2,10 +2,11 @@
   "credentials": [
     {
       "token": "",
+      "sshKey": "",
       "password": "admin",
       "user": "admin",
       "label": "Zabbix",
       "id": "credential1"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt
index 2705faf5d..8bcb65644 100644
--- a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt
+++ b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt
@@ -37,10 +37,15 @@ internal class EndpointTest {
         assert(validEndpoint.containsKey("user"))
         assert(validEndpoint.containsKey("password"))
         assert(validEndpoint.containsKey("token"))
+        assert(validEndpoint.containsKey("sshKey"))
+        assert(validEndpoint.containsKey("sshKeyPassphrase"))
 
         assert(invalidEndpoint.containsKey("user"))
         assert(invalidEndpoint.containsKey("password"))
         assert(invalidEndpoint.containsKey("token"))
+        assert(invalidEndpoint.containsKey("sshKey"))
+        assert(invalidEndpoint.containsKey("sshKeyPassphrase"))
+
     }
 
     @Test
@@ -48,6 +53,9 @@ internal class EndpointTest {
         assertEquals("user1", validEndpoint.getString("user"))
         assertEquals("password1", validEndpoint.getString("password"))
         assertEquals("token1", validEndpoint.getString("token"))
+        assertEquals("key1", validEndpoint.getString("sshKey"))
+        assertEquals("pass1", validEndpoint.getString("sshKeyPassphrase"))
+
     }
 
     @Test
@@ -55,6 +63,8 @@ internal class EndpointTest {
         assertEquals("", invalidEndpoint.getString("user"))
         assertEquals("", invalidEndpoint.getString("password"))
         assertEquals("", invalidEndpoint.getString("token"))
+        assertEquals("", invalidEndpoint.getString("sshKey"))
+        assertEquals("", invalidEndpoint.getString("sshKeyPassphrase"))
     }
 
     @Test
@@ -67,7 +77,9 @@ internal class EndpointTest {
                       "publicUrl" : "Public Url",
                       "user" : "user1",
                       "password" : "password1",
-                      "token" : "token1"
+                      "token" : "token1",
+                      "sshKey": "key1",
+                      "sshKeyPassphrase" : "pass1"
                     }
                     """)
 
@@ -83,7 +95,9 @@ internal class EndpointTest {
                       "url" : "url",
                       "user" : "",
                       "password" : "",
-                      "token" : ""
+                      "token" : "",
+                      "sshKey" : "",
+                      "sshKeyPassphrase" : ""
                     }
                     """)
 
diff --git a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerTest.kt b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerTest.kt
new file mode 100644
index 000000000..a06401d31
--- /dev/null
+++ b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerTest.kt
@@ -0,0 +1,63 @@
+package com.cognifide.cogboard.widget.type.logviewer
+
+import com.cognifide.cogboard.CogboardConstants.Props
+import com.cognifide.cogboard.CogboardConstants.RequestMethod
+import com.cognifide.cogboard.widget.type.WidgetTestBase
+import io.vertx.core.buffer.Buffer
+import io.vertx.core.eventbus.MessageConsumer
+import io.vertx.core.json.JsonObject
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.mockito.Mockito.*
+
+class LogViewerTest: WidgetTestBase() {
+
+    private lateinit var widget: LogViewerWidget
+
+    override fun widgetName(): String {
+        return "LogViewerWidget"
+    }
+
+    @BeforeEach
+    fun initForTest() {
+        super.init()
+    }
+
+    @Test
+    fun `Expect Buffer consumer to be used when type is SSH`() {
+        val consumerMock = mock(MessageConsumer::class.java) as MessageConsumer<Buffer>
+        `when`(eventBus.consumer<Buffer>(anyString())).thenReturn(consumerMock)
+
+        val endpoint = mockEndpointData("ssh")
+
+        val config = initWidget()
+                .put(Props.ENDPOINT_LOADED, endpoint)
+                .put(Props.LOG_LINES, "5")
+        widget = LogViewerWidget(vertx, config, initService())
+
+        widget.start()
+
+        verify(eventBus).consumer<Buffer>(eq(widget.eventBusAddress))
+    }
+
+    @Test
+    fun `Expect JsonObject consumer to be used when type is HTTP`() {
+        val consumerMock = mock(MessageConsumer::class.java) as MessageConsumer<JsonObject>
+        `when`(eventBus.consumer<JsonObject>(anyString())).thenReturn(consumerMock)
+        
+        val endpoint = mockEndpointData("http")
+
+        val config = initWidget()
+                .put(Props.LOG_REQUEST_TYPE, RequestMethod.GET)
+                .put(Props.ENDPOINT_LOADED, endpoint)
+                .put(Props.LOG_LINES, "5")
+        widget = LogViewerWidget(vertx, config, initService())
+
+        widget.start()
+
+        verify(eventBus).consumer<JsonObject>(eq(widget.eventBusAddress))
+    }
+    
+    private fun mockEndpointData(protocol: String): JsonObject =
+            JsonObject(mapOf(Pair(Props.URL, "$protocol://192.168.0.1")))
+}
diff --git a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategyTest.kt b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategyTest.kt
new file mode 100644
index 000000000..bb56a0fd9
--- /dev/null
+++ b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategyTest.kt
@@ -0,0 +1,30 @@
+package com.cognifide.cogboard.widget.type.logviewer.logparser
+
+import org.junit.jupiter.api.Test
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.TYPE
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.DATE
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.VARIABLE_DATA
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.HEADERS
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.TEMPLATE
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.PROVIDER
+import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.MESSAGE
+
+class MockLogParserStrategyTest {
+    private val sampleLog = "2021-11-06:22:40:25 *DEBUG* [FelixStartLevel]  Integer lobortis. bibendum Nulla mi"
+    private val parser = MockLogParserStrategy()
+
+    @Test
+    fun parseSampleLog() {
+        val output = parser.parseLine(sampleLog)
+        val variableData = output.getJsonObject(VARIABLE_DATA)
+        val template = variableData.getJsonArray(TEMPLATE)
+        val headers = variableData.getJsonArray(HEADERS)
+
+        assert(output.getString(TYPE) == "DEBUG")
+        assert(output.getString(DATE) == "2021-11-06:22:40:25")
+        assert(template.getString(0) == PROVIDER)
+        assert(template.getString(1) == MESSAGE)
+        assert(headers.getString(0) == "FelixStartLevel")
+        assert(headers.getString(1) == "Integer lobortis. bibendum Nulla mi")
+    }
+}
\ No newline at end of file
diff --git a/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json b/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json
index 9cdcfe54e..b5b431094 100644
--- a/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json
+++ b/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json
@@ -5,14 +5,18 @@
       "label": "My Credentials 1",
       "user": "user1",
       "password": "password1",
-      "token": "token1"
+      "token": "token1",
+      "sshKey": "key1",
+      "sshKeyPassphrase": "pass1"
     },
     {
       "id": "credentials2",
       "label": "My Credentials 2",
       "user": "user2",
       "password": "password2",
-      "token": "token2"
+      "token": "token2",
+      "sshKey": "key2",
+      "sshKeyPassphrase": "pass2"
     }
   ]
 }
diff --git a/cogboard-local-compose.yml b/cogboard-local-compose.yml
index 327aeb6f9..f9b6fa54a 100644
--- a/cogboard-local-compose.yml
+++ b/cogboard-local-compose.yml
@@ -16,6 +16,12 @@ services:
       - cognet
     command: ["--no-request-journal", "--global-response-templating"]
 
+  ssh-server:
+    image: ssh-server
+    hostname: ssh-server
+    networks:
+      - cognet
+
   backend:
     image: "cogboard/cogboard-app:${COGBOARD_VERSION}"
     environment:
diff --git a/cogboard-webapp/src/components/CredentialForm/index.js b/cogboard-webapp/src/components/CredentialForm/index.js
index 8cc72821a..bd8757fb6 100644
--- a/cogboard-webapp/src/components/CredentialForm/index.js
+++ b/cogboard-webapp/src/components/CredentialForm/index.js
@@ -22,7 +22,9 @@ const CredentialsForm = ({
     'UsernameField',
     'PasswordField',
     'PasswordConfirmationField',
-    'TokenField'
+    'TokenField',
+    'SSHKeyField',
+    'SSHKeyPassphraseField'
   ];
 
   const constraints = {
@@ -82,7 +84,9 @@ CredentialsForm.defaultProps = {
   user: '',
   password: '',
   confirmationPassword: '',
-  token: ''
+  token: '',
+  sshKey: '',
+  sshKeyPassphrase: ''
 };
 
 export default CredentialsForm;
diff --git a/cogboard-webapp/src/components/widgets/dialogFields/FileTextInput.js b/cogboard-webapp/src/components/widgets/dialogFields/FileTextInput.js
new file mode 100644
index 000000000..e4b774f2f
--- /dev/null
+++ b/cogboard-webapp/src/components/widgets/dialogFields/FileTextInput.js
@@ -0,0 +1,60 @@
+import React, { useState } from 'react';
+
+import { Button } from '@material-ui/core';
+import { StyledValidationMessages } from '../../WidgetForm/styled';
+import {
+  DeleteButton,
+  StyledHorizontalStack,
+  StyledLabel,
+  StyledVerticalStack
+} from './styled';
+
+const FileTextInput = ({ error, dataCy, onChange }) => {
+  const [filename, setFilename] = useState('');
+
+  const getFileContents = async event => {
+    event.preventDefault();
+    const file = event.target.files[0];
+    const reader = new FileReader();
+    reader.onload = async event => {
+      const text = event.target.result;
+      event.target.value = text;
+      onChange(event);
+    };
+    reader.readAsText(file);
+    setFilename(file.name);
+  };
+
+  const deleteFile = event => {
+    event.preventDefault();
+    setFilename('');
+    event.target.value = '';
+    onChange(event);
+  };
+
+  const fileInfo =
+    filename === '' ? null : (
+      <StyledHorizontalStack>
+        <p>{filename}</p>
+        <DeleteButton variant="contained" onClick={e => deleteFile(e)}>
+          Delete
+        </DeleteButton>
+      </StyledHorizontalStack>
+    );
+
+  return (
+    <StyledVerticalStack>
+      <StyledLabel>SSH Key</StyledLabel>
+      <StyledHorizontalStack>
+        <Button variant="contained" component="label">
+          Upload a File
+          <input type="file" hidden onChange={e => getFileContents(e)} />
+        </Button>
+        {fileInfo}
+      </StyledHorizontalStack>
+      <StyledValidationMessages messages={error} data-cy={`${dataCy}-error`} />
+    </StyledVerticalStack>
+  );
+};
+
+export default FileTextInput;
diff --git a/cogboard-webapp/src/components/widgets/dialogFields/index.js b/cogboard-webapp/src/components/widgets/dialogFields/index.js
index 2ec23c533..c5891e036 100644
--- a/cogboard-webapp/src/components/widgets/dialogFields/index.js
+++ b/cogboard-webapp/src/components/widgets/dialogFields/index.js
@@ -37,6 +37,7 @@ import RangeSlider from './RangeSlider';
 import LinkListInput from './LinkListInput';
 import ToDoListInput from './ToDoListinput';
 import WidgetTypeField from './WidgetTypeField';
+import FileTextInput from './FileTextInput';
 
 const dialogFields = {
   LabelField: {
@@ -100,12 +101,33 @@ const dialogFields = {
     label: 'Token',
     validator: () => string()
   },
+  SSHKeyField: {
+    component: FileTextInput,
+    name: 'sshKey',
+    label: 'SSH Private Key',
+    validator: () =>
+      string()
+        .matches('^-----BEGIN ([A-Z]{1,} )*PRIVATE KEY-----\n', {
+          message: vm.SSH_KEY_BEGIN,
+          excludeEmptyString: true
+        })
+        .matches('\n-----END ([A-Z]{1,} )*PRIVATE KEY-----\n$', {
+          message: vm.SSH_KEY_END,
+          excludeEmptyString: true
+        })
+  },
+  SSHKeyPassphraseField: {
+    component: PasswordInput,
+    name: 'sshKeyPassphrase',
+    label: 'SSH Private Key Passphrase',
+    validator: () => string()
+  },
   PublicURL: {
     component: TextInput,
     name: 'publicUrl',
     label: 'Public URL',
     validator: () =>
-      string().matches(/^(http|https|ws|ftp):\/\/.*([:.]).*/, {
+      string().matches(/^(http|https|ws|ftp|ssh):\/\/.*([:.]).*/, {
         message: vm.INVALID_PUBLIC_URL(),
         excludeEmptyString: true
       })
@@ -251,7 +273,7 @@ const dialogFields = {
     name: 'url',
     label: 'URL',
     validator: () =>
-      string().matches(/^(http|https|ws|ftp):\/\/.*([:.]).*/, {
+      string().matches(/^(http|https|ws|ftp|ssh):\/\/.*([:.]).*/, {
         message: vm.INVALID_URL(),
         excludeEmptyString: true
       })
diff --git a/cogboard-webapp/src/components/widgets/dialogFields/styled.js b/cogboard-webapp/src/components/widgets/dialogFields/styled.js
index 4d56781db..593893e84 100644
--- a/cogboard-webapp/src/components/widgets/dialogFields/styled.js
+++ b/cogboard-webapp/src/components/widgets/dialogFields/styled.js
@@ -2,7 +2,7 @@ import styled from '@emotion/styled/macro';
 import NumberInput from './NumberInput';
 import IntegerInput from './IntegerInput';
 import { COLORS } from '../../../constants';
-import { Box, Input, Fab, List, FormControl } from '@material-ui/core';
+import { Box, Input, Fab, List, FormControl, Button } from '@material-ui/core';
 
 export const StyledNumberInput = styled(NumberInput)`
   flex-basis: calc(50% - 18px);
@@ -152,3 +152,34 @@ export const StyledMultiLineWrapper = styled.div`
     flex: 1 0 auto;
   }
 `;
+
+export const StyledHorizontalStack = styled.div`
+  display: flex;
+  flex-direction: row;
+  gap: 12px;
+`;
+
+export const StyledVerticalStack = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+`;
+
+export const StyledLabel = styled.p`
+  font-size: 1rem;
+  margin: 0;
+  color: rgba(255, 255, 255, 0.7);
+  transform: translate(0, 1.5px) scale(0.75);
+  transform-origin: top left;
+  font-weight: 400;
+  line-height: 1;
+  letter-spacing: 0.00938em;
+`;
+
+export const DeleteButton = styled(Button)`
+  background-color: ${COLORS.RED};
+
+  &:hover {
+    background-color: ${COLORS.DARK_RED};
+  }
+`;
diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js
index 3999565ea..a56b7d487 100644
--- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js
+++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js
@@ -34,10 +34,10 @@ export default function LogEntry({ type, date, additionalData, variableData }) {
 
   const VariablePart = ({ description }) => {
     const variableFieldsTemplate = getGridTemplate(variableData.template);
-    const data = description ? variableData.description : variableData.header;
+    const data = description ? variableData.description : variableData.headers;
     return (
       <VariableGridSchema template={variableFieldsTemplate}>
-        {data.map((text, index) => (
+        {data?.map((text, index) => (
           <Text key={index}>{text}</Text>
         ))}
       </VariableGridSchema>
@@ -73,7 +73,7 @@ LogEntry.propTypes = {
   additionalData: objectOf(oneOfType([string, number, bool])),
   variableData: shape({
     template: arrayOf(string).isRequired,
-    header: arrayOf(oneOfType([string, number, bool])).isRequired,
+    headers: arrayOf(oneOfType([string, number, bool])).isRequired,
     description: arrayOf(oneOfType([string, number, bool])).isRequired
   })
 };
@@ -82,7 +82,7 @@ LogEntry.defaultProps = {
   type: 'info',
   variableData: {
     template: [],
-    header: [],
+    headers: [],
     description: []
   },
   additionalData: {}
diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js
index 5ff41576a..c85f9192a 100644
--- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js
+++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js
@@ -11,33 +11,11 @@ import {
 } from './styled';
 import getGridTemplate from './helpers';
 
-const testLogTemplate = ['Provider', 'Message'];
-const testData = {
-  date: '2021-04-22 14:08:37',
-  additionalData: {
-    ID: '123456',
-    Type: 'sys',
-    'IP address': '127.0.0.1',
-    Port: '27017'
-  },
-  variableData: {
-    template: testLogTemplate,
-    header: [
-      'mongodb.log',
-      'Expected corresponding JSX closing tag for <GridSchemaa>.'
-    ],
-    description: [
-      'provider desc',
-      'SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for <GridSchemaa>. (21:6) SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for <GridSchemaa>. (21:6) SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for <GridSchemaa>. (21:6) SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for <GridSchemaa>. (21:6)'
-    ]
-  }
-};
-
-export default function LogList() {
+export default function LogList({ logs, template }) {
   const theme = useTheme();
   const VariableLogListHeader = () => (
-    <VariableGridSchema template={getGridTemplate(testLogTemplate)}>
-      {testLogTemplate.map((name, index) => (
+    <VariableGridSchema template={getGridTemplate(template)}>
+      {template.map((name, index) => (
         <ColumnTitle key={index}>{name}</ColumnTitle>
       ))}
     </VariableGridSchema>
@@ -54,61 +32,15 @@ export default function LogList() {
       </Header>
 
       <LogsWrapper>
-        {/* static presentation */}
-        <LogEntry
-          type="info"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          type="warn"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          type="error"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          type="success"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry type="error" date={testData.date} />
-        <LogEntry
-          type="info"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          type="warn"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          type="error"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
-        <LogEntry
-          type="success"
-          date={testData.date}
-          additionalData={testData.additionalData}
-          variableData={testData.variableData}
-        />
+        {logs?.map((log, index) => (
+          <LogEntry
+            key={index}
+            type={log.type}
+            date={log.date}
+            additionalData={log.additionalData}
+            variableData={log.variableData}
+          />
+        ))}
       </LogsWrapper>
     </Container>
   );
diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js
index 2ff61248c..62669eaf6 100644
--- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js
+++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js
@@ -1,6 +1,7 @@
 import styled from '@emotion/styled/macro';
 import { COLORS } from '../../../../../constants';
 import { Typography, Accordion } from '@material-ui/core';
+import logLevels from '../logLevels';
 
 export const Container = styled.div`
   max-height: 100%;
@@ -36,23 +37,22 @@ export const ColumnTitle = styled(Typography)`
 `;
 
 export const Text = styled(Typography)(props => {
-  const getColor = type =>
-    ({
-      info: COLORS.WHITE,
-      success: COLORS.GREEN,
-      warn: COLORS.YELLOW,
-      error: COLORS.RED
-    }[type.toLowerCase()]);
+  let logTypeStyles = ``;
+  if (props.type) {
+    const logLevel = logLevels.find(
+      level => level.value === props.type?.toLowerCase()
+    );
+    logTypeStyles = `
+      font-weight: 500;
+      color: ${logLevel?.color || COLORS.WHITE};
+    `;
+  }
 
   return `
       line-height: 19px;
       font-size: 0.8rem;
       font-weight: 400;
-      ${props.type &&
-        `
-        font-weight: 500;
-        color: ${getColor(props.type)};
-      `}
+      ${logTypeStyles}
     `;
 });
 
diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js
index 1e6551d1b..851dc8415 100644
--- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js
+++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js
@@ -10,6 +10,7 @@ import {
 import { ScrollableBox } from './styled';
 import ToolbarGroup from '../ToolbarGroup';
 import { useState } from 'react';
+import logLevels from '../../logLevels';
 
 const FilterPicker = () => {
   const handleDelete = name => {
@@ -17,7 +18,7 @@ const FilterPicker = () => {
   };
 
   const [filters, setFilters] = useState([]);
-  const [logLevel, setLogLevel] = useState('');
+  const [logLevel, setLogLevel] = useState('info');
 
   return (
     <ToolbarGroup title="Filters">
@@ -48,11 +49,11 @@ const FilterPicker = () => {
             </ScrollableBox>
           )}
         >
-          <MenuItem value="all">ALL</MenuItem>
-          <MenuItem value="debug">DEBUG</MenuItem>
-          <MenuItem value="info">INFO</MenuItem>
-          <MenuItem value="warn">WARN</MenuItem>
-          <MenuItem value="error">ERROR</MenuItem>
+          {logLevels.map((level, index) => (
+            <MenuItem key={index} value={level.value}>
+              {level.value.toUpperCase()}
+            </MenuItem>
+          ))}
         </Select>
       </FormControl>
 
@@ -65,11 +66,11 @@ const FilterPicker = () => {
           value={logLevel}
           onChange={e => setLogLevel(e.target.value)}
         >
-          <MenuItem value="">ALL</MenuItem>
-          <MenuItem value="debug">DEBUG</MenuItem>
-          <MenuItem value="info">INFO</MenuItem>
-          <MenuItem value="warn">WARN</MenuItem>
-          <MenuItem value="error">ERROR</MenuItem>
+          {logLevels.map((level, index) => (
+            <MenuItem key={index} value={level.value}>
+              {level.value.toUpperCase()}
+            </MenuItem>
+          ))}
         </Select>
       </FormControl>
       <Button variant="contained" size="small">
diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/index.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/index.js
index 512f33285..d7336f061 100644
--- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/index.js
+++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/index.js
@@ -1,15 +1,25 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { shallowEqual, useSelector } from 'react-redux';
 import { number, string } from 'prop-types';
 import Toolbar from './Toolbar';
 import LogList from './LogList';
 import { Container } from './styled';
 
-const LogViewerWidget = () => (
-  <Container>
-    <Toolbar />
-    <LogList />
-  </Container>
-);
+const LogViewerWidget = ({ id }) => {
+  const widgetData = useSelector(
+    ({ widgets }) => widgets.widgetsById[id],
+    shallowEqual
+  );
+  useEffect(() => console.log(widgetData), [widgetData]);
+
+  const logs = widgetData.content?.logs;
+  return (
+    <Container>
+      <Toolbar />
+      {logs && <LogList logs={logs} template={logs[0].variableData.template} />}
+    </Container>
+  );
+};
 
 LogViewerWidget.propTypes = {
   endpoint: string,
diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/logLevels.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/logLevels.js
new file mode 100644
index 000000000..c5ff784bf
--- /dev/null
+++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/logLevels.js
@@ -0,0 +1,11 @@
+import { COLORS } from '../../../../constants';
+
+const logLevels = [
+  { value: 'debug', color: COLORS.WHITE },
+  { value: 'info', color: COLORS.WHITE },
+  { value: 'warning', color: COLORS.YELLOW },
+  { value: 'error', color: COLORS.RED },
+  { value: 'success', color: COLORS.GREEN }
+];
+
+export default logLevels;
diff --git a/cogboard-webapp/src/constants/index.js b/cogboard-webapp/src/constants/index.js
index 3b83785d9..07da55449 100644
--- a/cogboard-webapp/src/constants/index.js
+++ b/cogboard-webapp/src/constants/index.js
@@ -305,7 +305,9 @@ export const validationMessages = {
   INVALID_PUBLIC_URL: () => 'Invalid Public URL',
   FIELD_MIN_ITEMS: () => 'This field must have at least 1 item.',
   UNIQUE_FIELD: () => 'This field must be unique.',
-  PASSWORD_MATCH: () => 'Password must match.'
+  PASSWORD_MATCH: () => 'Password must match.',
+  SSH_KEY_BEGIN: () => 'The key must begin with "-----BEGIN PRIVATE KEY-----"',
+  SSH_KEY_END: () => 'The key must end with "-----END PRIVATE KEY-----"'
 };
 
 export const NOTIFICATIONS = {
diff --git a/functional/cypress-tests/cypress/fixtures/credentialsEndpoints.js b/functional/cypress-tests/cypress/fixtures/credentialsEndpoints.js
index 03e0430b5..39b2431bb 100644
--- a/functional/cypress-tests/cypress/fixtures/credentialsEndpoints.js
+++ b/functional/cypress-tests/cypress/fixtures/credentialsEndpoints.js
@@ -4,7 +4,8 @@ export const badCredentials = () => {
     password: 'xxxxxxxxxxx',
     passwordConf: 'zzz',
     user: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
-    label: ' '
+    label: ' ',
+    sshKeyPassphrase: ''
   };
 };
 
diff --git a/functional/cypress-tests/cypress/integration/credentials.js b/functional/cypress-tests/cypress/integration/credentials.js
index 572afbbf4..90adcc8ca 100644
--- a/functional/cypress-tests/cypress/integration/credentials.js
+++ b/functional/cypress-tests/cypress/integration/credentials.js
@@ -32,7 +32,7 @@ describe('Credentials', () => {
       .assertErrorMessageVisible(
         'Label length must be less or equal to 25.',
         'credential-form-auth-user-input-error'
-      );
+      )
   });
 
   it('User can add new credentials without username, password and token.', () => {
diff --git a/functional/cypress-tests/cypress/support/credential.js b/functional/cypress-tests/cypress/support/credential.js
index 9ee77c41f..7028edfd7 100644
--- a/functional/cypress-tests/cypress/support/credential.js
+++ b/functional/cypress-tests/cypress/support/credential.js
@@ -58,6 +58,17 @@ class Credentials {
     return this;
   }
 
+  applySSHKeyPassphrase(config) {
+    if (config !== undefined) {
+      this.config = config;
+    }
+    cy.get('[data-cy="credential-form-auth-ssh-key-passphrase-input"]')
+      .clear()
+      .type(this.config.sshKeyPassphrase)
+      .blur();
+    return this;
+  }
+
   save() {
     cy.get('[data-cy="credential-form-submit-button"]').click();
     return this;
@@ -77,7 +88,9 @@ class Credentials {
   }
 
   assertErrorMessageVisible(message, dataCYName) {
-    cy.contains(`[data-cy^="${dataCYName}"]`, message).should('is.visible');
+    cy.contains(`[data-cy^="${dataCYName}"]`, message)
+      .scrollIntoView()
+      .should('is.visible');
     return this;
   }
 }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0700e243a..dd77ec502 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,3 +17,4 @@ rootProject.name = "cogboard"
 
 include("cogboard-app")
 include("cogboard-webapp")
+include("ssh")
\ No newline at end of file
diff --git a/ssh/Dockerfile b/ssh/Dockerfile
new file mode 100644
index 000000000..34debc3ee
--- /dev/null
+++ b/ssh/Dockerfile
@@ -0,0 +1,9 @@
+FROM lscr.io/linuxserver/openssh-server
+
+ENV PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICLzgU4PuqFGnQCd5A5Gl30ssE3b9fW7gcyvES4xHHUo mock@openssh-server"
+ENV PASSWORD_ACCESS=true
+ENV USER_NAME=mock
+ENV USER_PASSWORD=TLQuoLMn*T89&Y*r*YqHviSFH6MkR!4E
+
+ADD gen.sh /home/mock/gen.sh
+RUN /home/mock/gen.sh 50 > /home/mock/example.txt
\ No newline at end of file
diff --git a/ssh/build.gradle.kts b/ssh/build.gradle.kts
new file mode 100644
index 000000000..13232d3c4
--- /dev/null
+++ b/ssh/build.gradle.kts
@@ -0,0 +1,14 @@
+import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
+
+plugins {
+    id("base")
+    id("com.bmuschko.docker-remote-api")
+}
+
+tasks {
+    register<DockerBuildImage>("buildImage") {
+        group = "docker"
+        inputDir.set(file(projectDir))
+        images.add("ssh-server")
+    }
+}
\ No newline at end of file
diff --git a/ssh/gen.sh b/ssh/gen.sh
new file mode 100755
index 000000000..b4b511bf7
--- /dev/null
+++ b/ssh/gen.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+labels=(DEBUG INFO WARNING ERROR)
+words=(Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet massa sit amet mi feugiat lobortis. Morbi ultrices hendrerit luctus. Donec fermentum viverra viverra. Integer convallis sapien sit amet facilisis pretium. Ut quis commodo odio, ac bibendum urna. Ut imperdiet ante sed ex sollicitudin auctor. Nulla vel eros sit amet velit vulputate suscipit a malesuada felis. Curabitur commodo, erat eget condimentum tristique, ante diam porttitor nulla, condimentum vulputate nibh ex sit amet velit. Vestibulum vel lectus bibendum, pellentesque nisl id, vehicula tellus. Suspendisse sem turpis, dignissim quis aliquet nec, laoreet sit amet urna. Nulla non euismod tellus, id varius)
+
+COUNT=5
+if ! [ -z "$1" ]; then
+    COUNT=$1
+fi
+
+for run in $( seq 1 $COUNT ); do
+    d=$(date +%Y-%m-%d:%H:%M:%S)
+    l=${labels[$(($RANDOM%4))]}
+    w=()
+    wCount=$(($RANDOM%5+5))
+    for (( i=0; i<$wCount; i++ ))
+    do
+            w[i]=${words[$(($RANDOM%${#words[@]}))]}
+    done
+    echo $d "*"${l}"* [FelixStartLevel] " ${w[*]}
+done
\ No newline at end of file