diff --git a/frontend/src/metabase/containers/Form.jsx b/frontend/src/metabase/containers/Form.jsx
index 2784d1299eca09970839baa2e8efb63fb37b4f5f..4a12d1a425c5a3ca717b984d9e5492c89aa2b0f9 100644
--- a/frontend/src/metabase/containers/Form.jsx
+++ b/frontend/src/metabase/containers/Form.jsx
@@ -181,11 +181,29 @@ export default class Form extends React.Component {
       formDef => makeFormObject(formDef),
     );
     const getInitialValues = createSelector(
-      [getFormObject, (state, props) => props.initialValues || {}],
-      (formObject, initialValues) => ({
-        ...formObject.initial(),
-        ...initialValues,
-      }),
+      [
+        getFormObject,
+        (state, props) => props.initialValues || {},
+        (state, props) => props.values || {},
+      ],
+      (formObject, initialValues, values) => {
+        const formInitialValues = formObject.initial(values);
+        // merge nested fields: {details: {foo: 123}} + {details: {bar: 321}} => {details: {foo: 123, bar: 321}}
+        const merged = {};
+        for (const k of Object.keys(initialValues)) {
+          if (
+            typeof initialValues[k] === "object" &&
+            typeof formInitialValues[k] === "object"
+          ) {
+            merged[k] = { ...formInitialValues[k], ...initialValues[k] };
+          }
+        }
+        return {
+          ...initialValues,
+          ...formInitialValues,
+          ...merged,
+        };
+      },
     );
     const getFieldNames = createSelector(
       [getFormObject, getInitialValues, (state, props) => props.values || {}],
@@ -215,18 +233,16 @@ export default class Form extends React.Component {
   };
 
   componentDidUpdate(prevProps: Props, prevState: State) {
-    if (!this.props.form) {
-      // HACK: when new fields are added they aren't initialized with their intialValues, so we have to force it here:
-      const newFields = _.difference(
-        Object.keys(this.state.inlineFields),
-        Object.keys(prevState.inlineFields),
+    // HACK: when new fields are added they aren't initialized with their intialValues, so we have to force it here:
+    const newFields = _.difference(
+      Object.keys(this.state.inlineFields),
+      Object.keys(prevState.inlineFields),
+    );
+    if (newFields.length > 0) {
+      // $FlowFixMe: dispatch provided by connect
+      this.props.dispatch(
+        initialize(this.props.formName, this._getInitialValues(), newFields),
       );
-      if (newFields.length > 0) {
-        // $FlowFixMe: dispatch provided by connect
-        this.props.dispatch(
-          initialize(this.props.formName, this._getInitialValues(), newFields),
-        );
-      }
     }
   }
 
@@ -314,6 +330,7 @@ export default class Form extends React.Component {
     return (
       <ReduxFormComponent
         {...this.props}
+        overwriteOnInitialValuesChange={false}
         formObject={formObject}
         // redux-form props:
         form={formName}
diff --git a/frontend/src/metabase/css/core/base.css b/frontend/src/metabase/css/core/base.css
index 589676e79f98bd50ba497b118d1929539029d794..2526b7f14dc665e3878747c145d21806ca05c1a4 100644
--- a/frontend/src/metabase/css/core/base.css
+++ b/frontend/src/metabase/css/core/base.css
@@ -117,6 +117,10 @@ textarea {
     sans-serif;
 }
 
+textarea {
+  min-height: 110px;
+}
+
 .pointer-events-none {
   pointer-events: none;
 }
diff --git a/frontend/src/metabase/entities/databases/forms.js b/frontend/src/metabase/entities/databases/forms.js
index 3942d48be0b528e2364f8370dc63aa6a4eb4bc9a..f3cc2951dff46d613fde0229dd2f68ba7ee58c0e 100644
--- a/frontend/src/metabase/entities/databases/forms.js
+++ b/frontend/src/metabase/entities/databases/forms.js
@@ -45,6 +45,21 @@ const DATABASE_DETAIL_OVERRIDES = {
       return null;
     },
   }),
+  "tunnel-private-key": (engine, details) => ({
+    title: t`SSH private key`,
+    placeholder: t`Paste the contents of your ssh private key here`,
+    type: "text",
+  }),
+  "tunnel-private-key-passphrase": (engine, details) => ({
+    title: t`Passphrase for the SSH private key`,
+  }),
+  "tunnel-auth-option": (engine, details) => ({
+    title: t`SSH Authentication`,
+    options: [
+      { name: t`SSH Key`, value: "ssh-key" },
+      { name: t`Password`, value: "password" },
+    ],
+  }),
 };
 
 const AUTH_URL_PREFIXES = {
@@ -167,6 +182,24 @@ function getFieldsForEngine(engine, details) {
       ) {
         continue;
       }
+
+      // hide the auth settings based on which auth method is selected
+      // private key auth needs tunnel-private-key and tunnel-private-key-passphrase
+      if (
+        field.name.startsWith("tunnel-private-") &&
+        details["tunnel-auth-option"] !== "ssh-key"
+      ) {
+        continue;
+      }
+
+      // username / password auth uses tunnel-pass
+      if (
+        field.name === "tunnel-pass" &&
+        details["tunnel-auth-option"] === "ssh-key"
+      ) {
+        continue;
+      }
+
       const overrides = DATABASE_DETAIL_OVERRIDES[field.name];
       // convert database details-fields to Form fields
       fields.push({
@@ -174,6 +207,7 @@ function getFieldsForEngine(engine, details) {
         title: field["display-name"],
         type: field.type,
         placeholder: field.placeholder || field.default,
+        options: field.options,
         validate: value => (field.required && !value ? t`required` : null),
         normalize: value =>
           value === "" || value == null
@@ -182,6 +216,7 @@ function getFieldsForEngine(engine, details) {
               : null
             : value,
         horizontal: field.type === "boolean",
+        initial: field.default,
         ...(overrides && overrides(engine, details)),
       });
     }
diff --git a/modules/drivers/druid/test/metabase/driver/druid/client_test.clj b/modules/drivers/druid/test/metabase/driver/druid/client_test.clj
index b252e345c9a97682f2660f63379e0ef2a557df6b..3289525d07af65d749f1621a5bc281e8deec4f52 100644
--- a/modules/drivers/druid/test/metabase/driver/druid/client_test.clj
+++ b/modules/drivers/druid/test/metabase/driver/druid/client_test.clj
@@ -31,26 +31,30 @@
                    (mt/wait-for-result cancel-chan 2000)))))))))
 
 (deftest ssh-tunnel-test
-  (mt/test-driver :druid
-    (let [engine  :druid
-          details {:ssl            false
-                   :password       "changeme"
-                   :tunnel-host    "localhost"
-                   :tunnel-pass    "BOGUS-BOGUS"
-                   :port           5432
-                   :dbname         "test"
-                   :host           "http://localhost"
-                   :tunnel-enabled true
-                   :tunnel-port    22
-                   :tunnel-user    "bogus"}]
-      (is (thrown?
-           com.jcraft.jsch.JSchException
-           (try
-             (tu.log/suppress-output
-               (driver.u/can-connect-with-details? engine details :throw-exceptions))
-             (catch Throwable e
-               (loop [^Throwable e e]
-                 (or (when (instance? com.jcraft.jsch.JSchException e)
-                       (throw e)
-                       e)
-                     (some-> (ex-cause e) recur))))))))))
+  (mt/test-driver
+   :druid
+   (is (thrown?
+        java.net.ConnectException
+        (try
+          (let [engine  :druid
+                details {:ssl            false
+                         :password       "changeme"
+                         :tunnel-host    "localhost"
+                         :tunnel-pass    "BOGUS-BOGUS"
+                         :port           5432
+                         :dbname         "test"
+                         :host           "http://localhost"
+                         :tunnel-enabled true
+                         ;; we want to use a bogus port here on purpose -
+                         ;; so that locally, it gets a ConnectionRefused,
+                         ;; and in CI it does too. Apache's SSHD library
+                         ;; doesn't wrap every exception in an SshdException
+                         :tunnel-port    21212
+                         :tunnel-user    "bogus"}]
+            (tu.log/suppress-output
+             (driver.u/can-connect-with-details? engine details :throw-exceptions)))
+          (catch Throwable e
+            (loop [^Throwable e e]
+              (or (when (instance? java.net.ConnectException e)
+                    (throw e))
+                  (some-> (.getCause e) recur)))))))))
diff --git a/modules/drivers/mongo/src/metabase/driver/mongo.clj b/modules/drivers/mongo/src/metabase/driver/mongo.clj
index e1abacd7973446f0a96774e89821baea14451f80..ff317912a1d7db1d64d8bac016e453f1a54551bd 100644
--- a/modules/drivers/mongo/src/metabase/driver/mongo.clj
+++ b/modules/drivers/mongo/src/metabase/driver/mongo.clj
@@ -71,10 +71,10 @@
     #"^Password can not be null when the authentication mechanism is unspecified$"
     (driver.common/connection-error-messages :password-required)
 
-    #"^com.jcraft.jsch.JSchException: Auth fail$"
+    #"^org.apache.sshd.common.SshException: No more authentication methods available$"
     (driver.common/connection-error-messages :ssh-tunnel-auth-fail)
 
-    #".*JSchException: java.net.ConnectException: Connection refused.*"
+    #"^java.net.ConnectException: Connection refused$"
     (driver.common/connection-error-messages :ssh-tunnel-connection-fail)
 
     #".*"                               ; default
diff --git a/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj b/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj
index 99d2e0f490c013a81188efc3141b293af9362986..d29a64406ff75c51702a900b947dee40b656aea0 100644
--- a/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj
+++ b/modules/drivers/mongo/test/metabase/driver/mongo/util_test.clj
@@ -1,7 +1,9 @@
 (ns metabase.driver.mongo.util-test
-  (:require [expectations :refer [expect]]
+  (:require [clojure.test :refer :all]
+            [expectations :refer [expect]]
             [metabase.driver.mongo.util :as mongo-util]
             [metabase.driver.util :as driver.u]
+            [metabase.test :as mt]
             [metabase.test.util.log :as tu.log])
   (:import [com.mongodb DB MongoClient MongoClientException ReadPreference ServerAddress]))
 
@@ -213,24 +215,32 @@
   (-> (#'mongo-util/connection-options-builder :additional-options "readPreference=ternary")
       .build))
 
-(expect
-  com.jcraft.jsch.JSchException
-  (try
-    (let [engine :mongo
-          details {:ssl            false
-                   :password       "changeme"
-                   :tunnel-host    "localhost"
-                   :tunnel-pass    "BOGUS-BOGUS"
-                   :port           5432
-                   :dbname         "test"
-                   :host           "localhost"
-                   :tunnel-enabled true
-                   :tunnel-port    22
-                   :tunnel-user    "bogus"}]
-      (tu.log/suppress-output
-        (driver.u/can-connect-with-details? engine details :throw-exceptions)))
-    (catch Throwable e
-      (loop [^Throwable e e]
-        (or (when (instance? com.jcraft.jsch.JSchException e)
-              e)
-            (some-> (.getCause e) recur))))))
+(deftest test-ssh-connection
+  (testing "Gets an error when it can't connect to mongo via ssh tunnel"
+    (mt/test-driver
+      :mongo
+      (is (thrown?
+           java.net.ConnectException
+           (try
+             (let [engine :mongo
+                   details {:ssl            false
+                            :password       "changeme"
+                            :tunnel-host    "localhost"
+                            :tunnel-pass    "BOGUS-BOGUS"
+                            :port           5432
+                            :dbname         "test"
+                            :host           "localhost"
+                            :tunnel-enabled true
+                            ;; we want to use a bogus port here on purpose -
+                            ;; so that locally, it gets a ConnectionRefused,
+                            ;; and in CI it does too. Apache's SSHD library
+                            ;; doesn't wrap every exception in an SshdException
+                            :tunnel-port    21212
+                            :tunnel-user    "bogus"}]
+               (tu.log/suppress-output
+                 (driver.u/can-connect-with-details? engine details :throw-exceptions)))
+             (catch Throwable e
+               (loop [^Throwable e e]
+                 (or (when (instance? java.net.ConnectException e)
+                       (throw e))
+                     (some-> (.getCause e) recur))))))))))
diff --git a/modules/drivers/oracle/test/metabase/driver/oracle_test.clj b/modules/drivers/oracle/test/metabase/driver/oracle_test.clj
index db4957ee865952edd3b24ffd0a32529ddc3818ce..4fdd69b9de9ca962b4f26fab2d96fb5b77a9fb1d 100644
--- a/modules/drivers/oracle/test/metabase/driver/oracle_test.clj
+++ b/modules/drivers/oracle/test/metabase/driver/oracle_test.clj
@@ -8,9 +8,11 @@
              [driver :as driver]
              [query-processor :as qp]
              [query-processor-test :as qp.test]
+             [test :as mt]
              [util :as u]]
             [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
             [metabase.driver.sql.query-processor :as sql.qp]
+            [metabase.driver.util :as driver.u]
             [metabase.models
              [field :refer [Field]]
              [table :refer [Table]]]
@@ -68,23 +70,35 @@
        (catch Throwable e
          (driver/humanize-connection-error-message :oracle (.getMessage e)))))
 
-(expect
-  com.jcraft.jsch.JSchException
-  (let [engine  :oracle
-        details {:ssl            false
-                 :password       "changeme"
-                 :tunnel-host    "localhost"
-                 :tunnel-pass    "BOGUS-BOGUS-BOGUS"
-                 :port           12345
-                 :service-name   "test"
-                 :sid            "asdf"
-                 :host           "localhost"
-                 :tunnel-enabled true
-                 :tunnel-port    22
-                 :user           "postgres"
-                 :tunnel-user    "example"}]
-    (tu.log/suppress-output
-      (driver/can-connect? :oracle details))))
+(deftest test-ssh-connection
+  (testing "Gets an error when it can't connect to oracle via ssh tunnel"
+    (mt/test-driver
+     :oracle
+     (is (thrown?
+          java.net.ConnectException
+          (try
+            (let [engine :oracle
+                  details {:ssl            false
+                           :password       "changeme"
+                           :tunnel-host    "localhost"
+                           :tunnel-pass    "BOGUS-BOGUS"
+                           :port           5432
+                           :dbname         "test"
+                           :host           "localhost"
+                           :tunnel-enabled true
+                           ;; we want to use a bogus port here on purpose -
+                           ;; so that locally, it gets a ConnectionRefused,
+                           ;; and in CI it does too. Apache's SSHD library
+                           ;; doesn't wrap every exception in an SshdException
+                           :tunnel-port    21212
+                           :tunnel-user    "bogus"}]
+              (tu.log/suppress-output
+               (driver.u/can-connect-with-details? engine details :throw-exceptions)))
+            (catch Throwable e
+              (loop [^Throwable e e]
+                (or (when (instance? java.net.ConnectException e)
+                      (throw e))
+                    (some-> (.getCause e) recur))))))))))
 
 (expect-with-driver :oracle
   "UTC"
diff --git a/modules/drivers/presto/test/metabase/driver/presto_test.clj b/modules/drivers/presto/test/metabase/driver/presto_test.clj
index f2c79585e22c4981ae474251a8502fccaaa00826..affa2eed78a8399bc4c4a37aa8ddda8096862ce4 100644
--- a/modules/drivers/presto/test/metabase/driver/presto_test.clj
+++ b/modules/drivers/presto/test/metabase/driver/presto_test.clj
@@ -139,23 +139,35 @@
     {:page {:page  2
             :items 5}}))
 
-(expect
-  "Hmm, we couldn't connect to the database. Make sure your host and port settings are correct"
-  (try
-    (let [details {:ssl            false
-                   :password       "changeme"
-                   :tunnel-host    "localhost"
-                   :tunnel-pass    "BOGUS-BOGUS"
-                   :catalog        "BOGUS"
-                   :host           "localhost"
-                   :port           9999
-                   :tunnel-enabled true
-                   :tunnel-port    22
-                   :tunnel-user    "bogus"}]
-      (tu.log/suppress-output
-        (driver.u/can-connect-with-details? :presto details :throw-exceptions)))
-    (catch Exception e
-      (.getMessage e))))
+(deftest test-connect-via-tunnel
+  (testing "connection fails as expected"
+    (mt/test-driver
+     :presto
+     (is (thrown?
+          java.net.ConnectException
+          (try
+            (let [engine :presto
+                  details {:ssl            false
+                           :password       "changeme"
+                           :tunnel-host    "localhost"
+                           :tunnel-pass    "BOGUS-BOGUS"
+                           :catalog        "BOGUS"
+                           :host           "localhost"
+                           :port           9999
+                           :tunnel-enabled true
+                           ;; we want to use a bogus port here on purpose -
+                           ;; so that locally, it gets a ConnectionRefused,
+                           ;; and in CI it does too. Apache's SSHD library
+                           ;; doesn't wrap every exception in an SshdException
+                           :tunnel-port    21212
+                           :tunnel-user    "bogus"}]
+              (tu.log/suppress-output
+               (driver.u/can-connect-with-details? engine details :throw-exceptions)))
+            (catch Throwable e
+              (loop [^Throwable e e]
+                (or (when (instance? java.net.ConnectException e)
+                      (throw e))
+                    (some-> (.getCause e) recur))))))))))
 
 (deftest db-default-timezone-test
   (mt/test-driver :presto
diff --git a/project.clj b/project.clj
index 9c57dcc979dced30b157839294e6faf20488d7cc..837191cd1efacfb1ce8cfabf71d52bf7ae04312d 100644
--- a/project.clj
+++ b/project.clj
@@ -77,7 +77,6 @@
     :exclusions [org.slf4j/slf4j-api
                  it.unimi.dsi/fastutil]]
    [com.draines/postal "2.0.3"]                                       ; SMTP library
-   [com.jcraft/jsch "0.1.55"]                                         ; SSH client for tunnels
    [com.google.guava/guava "28.2-jre"]                                ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
    [com.h2database/h2 "1.4.197"]                                      ; embedded SQL database
    [com.mattbertolini/liquibase-slf4j "2.0.0"]                        ; Java Migrations lib logging. We don't actually use this AFAIK (?)
@@ -112,6 +111,7 @@
    [metabase/throttle "1.0.2"]                                        ; Tools for throttling access to API endpoints and other code pathways
    [net.sf.cssbox/cssbox "4.12" :exclusions [org.slf4j/slf4j-api]]    ; HTML / CSS rendering
    [org.apache.commons/commons-lang3 "3.10"]                          ; helper methods for working with java.lang stuff
+   [org.apache.sshd/sshd-core "2.4.0"]                                ; ssh tunneling and test server
    [org.bouncycastle/bcprov-jdk15on "1.65"]                           ; Bouncy Castle crypto library -- explicit version of BC specified to resolve illegal reflective access errors
    [org.clojars.pntblnk/clj-ldap "0.0.16"]                            ; LDAP client
    [org.eclipse.jetty/jetty-server "9.4.27.v20200227"]                ; We require JDK 8 which allows us to run Jetty 9.4, ring-jetty-adapter runs on 1.7 which forces an older version
@@ -120,9 +120,10 @@
     :exclusions [ch.qos.logback/logback-classic]]
    [org.mariadb.jdbc/mariadb-java-client "2.5.1"]                     ; MySQL/MariaDB driver
    [org.postgresql/postgresql "42.2.8"]                               ; Postgres driver
-   [org.slf4j/slf4j-log4j12 "1.7.25"]                                 ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
+   [org.slf4j/slf4j-api "1.7.30"]                                     ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
+   [org.slf4j/slf4j-log4j12 "1.7.30"]                                 ; ^^
    [org.tcrawley/dynapath "1.1.0"]                                    ; Dynamically add Jars (e.g. Oracle or Vertica) to classpath
-   [org.threeten/threeten-extra "1.5.0"]                              ; extra Java 8 java.time classes like DayOfMonth and Quarter
+   [org.threeten/threeten-extra "1.5.0"]                               ; extra Java 8 java.time classes like DayOfMonth and Quarter
    [org.yaml/snakeyaml "1.23"]                                        ; YAML parser (required by liquibase)
    [potemkin "0.4.5" :exclusions [riddley]]                           ; utility macros & fns
    [pretty "1.0.4"]                                                   ; protocol for defining how custom types should be pretty printed
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index a1dfbe42748bf2d9f847791d2eddf150997f75ab..c35f4bb69b280093c8471372aeac66e82c0b3a13 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -283,7 +283,8 @@
     :display-name su/NonBlankString
 
     ;; Type of this property. Defaults to `:string` if unspecified.
-    (s/optional-key :type) (s/enum :string :integer :boolean :password)
+    ;; `:select` is a `String` in the backend.
+    (s/optional-key :type) (s/enum :string :integer :boolean :password :select :text)
 
     ;; A default value for this field if the user hasn't set an explicit value. This is shown in the UI as a
     ;; placeholder.
@@ -295,7 +296,10 @@
     (s/optional-key :placeholder) s/Any
 
     ;; Is this property required? Defaults to `false`.
-    (s/optional-key :required?) s/Bool}
+    (s/optional-key :required?) s/Bool
+
+    ;; Any options for `:select` types
+    (s/optional-key :options) {s/Keyword s/Str}}
 
    (complement (every-pred #(contains? % :default) #(contains? % :placeholder)))
    "connection details that does not have both default and placeholder"))
diff --git a/src/metabase/driver/common.clj b/src/metabase/driver/common.clj
index 4b7e6fde661a02b7d910b32b5a2674c28e836d5b..200f735bba49d5b1306205cf55f68225d60049bc 100644
--- a/src/metabase/driver/common.clj
+++ b/src/metabase/driver/common.clj
@@ -59,11 +59,11 @@
   "Map of the db host details field, useful for `connection-properties` implementations"
   {:name         "host"
    :display-name (deferred-tru "Host")
-   :default      "localhost"})
+   :placeholder  "localhost"})
 
 (def default-port-details
   "Map of the db port details field, useful for `connection-properties` implementations. Implementations should assoc a
-  `:default` key."
+  `:placeholder` key."
   {:name         "port"
    :display-name (deferred-tru "Port")
    :type         :integer})
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index 9f3fed287230f92187db73bf1f130ff60c850d7d..cef2f15577b55ef20e4d1c319bad1e5a7abd676b 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -83,7 +83,7 @@
   [_]
   (ssh/with-tunnel-config
     [driver.common/default-host-details
-     (assoc driver.common/default-port-details :default 3306)
+     (assoc driver.common/default-port-details :placeholder 3306)
      driver.common/default-dbname-details
      driver.common/default-user-details
      driver.common/default-password-details
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index d24ab97ee541feb883ae8b6701ef5e4b0cf7edb8..0214c3c4a1f0d01825e250118cd405a4edaefbae 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -86,7 +86,7 @@
   [_]
   (ssh/with-tunnel-config
     [driver.common/default-host-details
-     (assoc driver.common/default-port-details :default 5432)
+     (assoc driver.common/default-port-details :placeholder 5432)
      driver.common/default-dbname-details
      driver.common/default-user-details
      driver.common/default-password-details
diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj
index cf5cb4308c27234098c6d78c713f29c3e6f2cb29..1a16452b1d59bac84100e8cafe9bb4c82f9a8f48 100644
--- a/src/metabase/driver/sql_jdbc/connection.clj
+++ b/src/metabase/driver/sql_jdbc/connection.clj
@@ -87,14 +87,12 @@
   (let [details-with-tunnel (ssh/include-ssh-tunnel details) ;; If the tunnel is disabled this returned unchanged
         spec                (connection-details->spec driver details-with-tunnel)
         properties          (data-warehouse-connection-pool-properties driver)]
-    (assoc (connection-pool/connection-pool-spec spec properties)
-           :ssh-tunnel (:tunnel-connection details-with-tunnel))))
+    (connection-pool/connection-pool-spec spec properties)))
 
-(defn- destroy-pool! [database-id {:keys [ssh-tunnel], :as pool-spec}]
+(defn- destroy-pool! [database-id pool-spec]
   (log/debug (u/format-color 'red (trs "Closing old connection pool for database {0} ..." database-id)))
   (connection-pool/destroy-connection-pool! pool-spec)
-  (when ssh-tunnel
-    (.disconnect ^com.jcraft.jsch.Session ssh-tunnel)))
+  (ssh/close-tunnel! pool-spec))
 
 (defonce ^:private ^{:doc "A map of our currently open connection pools, keyed by Database `:id`."}
   database-id->connection-pool
diff --git a/src/metabase/util/ssh.clj b/src/metabase/util/ssh.clj
index 48cd586589233f44772e34f4bc2e0891065529ff..491177a65bfda7e5d549a81884ef250c90f3a43d 100644
--- a/src/metabase/util/ssh.clj
+++ b/src/metabase/util/ssh.clj
@@ -1,36 +1,66 @@
 (ns metabase.util.ssh
-  (:require [clojure.string :as str]
-            [clojure.tools.logging :as log]
+  (:require [clojure.tools.logging :as log]
             [metabase.util :as u])
-  (:import com.jcraft.jsch.JSch))
+  (:import java.io.ByteArrayInputStream
+           org.apache.sshd.client.future.ConnectFuture
+           org.apache.sshd.client.session.ClientSession
+           org.apache.sshd.client.session.forward.PortForwardingTracker
+           org.apache.sshd.client.SshClient
+           [org.apache.sshd.common.config.keys FilePasswordProvider FilePasswordProvider$ResourceDecodeResult]
+           org.apache.sshd.common.session.SessionHolder
+           org.apache.sshd.common.util.GenericUtils
+           org.apache.sshd.common.util.io.resource.AbstractIoResource
+           org.apache.sshd.common.util.net.SshdSocketAddress
+           org.apache.sshd.common.util.security.SecurityUtils
+           org.apache.sshd.server.forward.AcceptAllForwardingFilter))
 
 (def ^:private default-ssh-timeout 30000)
 
-(defn start-ssh-tunnel
-  "Opens a new ssh tunnel and returns the connection along with the dynamically
-   assigned tunnel entrance port. It's the callers responsibility to call .disconnect
-   on the returned connection object."
-  [{:keys [tunnel-host tunnel-port tunnel-user tunnel-pass host port]}]
-  (let [connection (doto ^com.jcraft.jsch.Session (.getSession (new com.jcraft.jsch.JSch)
-                                                               ^String tunnel-user
-                                                               ^String tunnel-host
-                                                               tunnel-port)
-                     (.setPassword ^String tunnel-pass)
-                     (.setConfig "StrictHostKeyChecking" "no")
-                     (.connect default-ssh-timeout)
-                     (.setPortForwardingL 0 host port))
-        input-port (some-> (.getPortForwardingL connection)
-                           first
-                           (str/split #":")
-                           first
-                           (Integer/parseInt))]
-    (assert (number? input-port))
-    (log/info (u/format-color 'cyan "creating ssh tunnel %s@%s:%s -L %s:%s:%s" tunnel-user tunnel-host tunnel-port input-port host port))
-    [connection input-port]))
+(def ^:private ^SshClient client
+  (doto (SshClient/setUpDefaultClient)
+    (.start)
+    (.setForwardingFilter AcceptAllForwardingFilter/INSTANCE)))
+
+(defn- maybe-add-tunnel-password!
+  [^ClientSession session ^String tunnel-pass]
+  (when tunnel-pass
+    (.addPasswordIdentity session tunnel-pass)))
+
+(defn- maybe-add-tunnel-private-key!
+  [^ClientSession session ^String tunnel-private-key tunnel-private-key-passphrase]
+  (when tunnel-private-key
+    (let [resource-key      (proxy [AbstractIoResource] [(class "key") "key"])
+          password-provider (proxy [FilePasswordProvider] []
+                              (getPassword [_ _ _]
+                                tunnel-private-key-passphrase)
+                              (handleDecodeAttemptResult [_ _ _ _ _]
+                                FilePasswordProvider$ResourceDecodeResult/TERMINATE))
+          ids               (with-open [is (ByteArrayInputStream. (.getBytes tunnel-private-key "UTF-8"))]
+                              (SecurityUtils/loadKeyPairIdentities session resource-key is password-provider))
+          keypair           (GenericUtils/head ids)]
+      (.addPublicKeyIdentity session keypair))))
+
+(defn start-ssh-tunnel!
+  "Opens a new ssh tunnel and returns the connection along with the dynamically assigned tunnel entrance port. It's the
+  callers responsibility to call `close-tunnel` on the returned connection object."
+  [{:keys [^String tunnel-host ^Integer tunnel-port ^String tunnel-user tunnel-pass tunnel-private-key
+           tunnel-private-key-passphrase host port]}]
+  (let [^ConnectFuture conn-future (.connect client tunnel-user tunnel-host tunnel-port)
+        ^SessionHolder conn-status (.verify conn-future default-ssh-timeout)
+        session                    (doto ^ClientSession (.getSession conn-status)
+                                     (maybe-add-tunnel-password! tunnel-pass)
+                                     (maybe-add-tunnel-private-key! tunnel-private-key tunnel-private-key-passphrase)
+                                     (.. auth (verify default-ssh-timeout)))
+        tracker                    (.createLocalPortForwardingTracker session
+                                                                      (SshdSocketAddress. "" 0)
+                                                                      (SshdSocketAddress. host port))
+        input-port                 (.. tracker getBoundAddress getPort)]
+    (log/trace (u/format-color 'cyan "creating ssh tunnel %s@%s:%s -L %s:%s:%s" tunnel-user tunnel-host tunnel-port input-port host port))
+    [session tracker]))
 
 (def ssh-tunnel-preferences
   "Configuration parameters to include in the add driver page on drivers that
-   support ssh tunnels"
+  support ssh tunnels"
   [{:name         "tunnel-enabled"
     :display-name "Use SSH tunnel"
     :placeholder  "Enable this ssh tunnel?"
@@ -49,19 +79,26 @@
     :display-name "SSH tunnel username"
     :placeholder  "What username do you use to login to the SSH tunnel?"
     :required     true}
+   ;; this is entirely a UI flag
+   {:name         "tunnel-auth-option"
+    :display-name "SSH Authentication"
+    :type         :select
+    :options      [{:name "SSH Key" :value "ssh-key"}
+                   {:name "Password" :value "password"}]
+    :default      "ssh-key"}
    {:name         "tunnel-pass"
     :display-name "SSH tunnel password"
     :type         :password
-    :placeholder  "******"
-    :required     true}
-   #_{:name         "tunnel-private-key"
+    :placeholder  "******"}
+   {:name         "tunnel-private-key"
     :display-name "SSH private key to connect to the tunnel"
     :type         :string
-    :placeholder  "Paste the contents of an ssh private key here"}
-   #_{:name         "tunnel-private-key-file-name"
-    :display-name "Path on the Metabase server to a SSH private key file to connect to the tunnel"
-    :type         :string
-    :placeholder  "/home/YOUR-USERNAME/.ssh/id_rsa"}])
+    :placeholder  "Paste the contents of an ssh private key here"
+    :required     true}
+   {:name         "tunnel-private-key-passphrase"
+    :display-name "Passphrase for SSH private key"
+    :type         :password
+    :placeholder  "******"}])
 
 (defn with-tunnel-config
   "Add preferences for ssh tunnels to a drivers :connection-properties"
@@ -75,34 +112,42 @@
 
 (defn include-ssh-tunnel
   "Updates connection details for a data warehouse to use the ssh tunnel host and port
-   For drivers that enter hosts including the protocol (https://host), copy the protocol over as well"
+  For drivers that enter hosts including the protocol (https://host), copy the protocol over as well"
   [details]
   (if (use-ssh-tunnel? details)
-    (let [[_ proto host] (re-find #"(.*://)?(.*)" (:host details))
-          [connection tunnel-entrance-port] (start-ssh-tunnel (assoc details :host host)) ;; don't include L7 protocol in ssh tunnel
-          details-with-tunnel (assoc details
-                                :port tunnel-entrance-port ;; This parameter is set dynamically when the connection is established
-                                :host (str proto "localhost")
-                                :tunnel-entrance-port tunnel-entrance-port ;; the input port is not known until the connection is opened
-                                :tunnel-connection connection)]
+    (let [[_ proto host]                           (re-find #"(.*://)?(.*)" (:host details))
+          [session ^PortForwardingTracker tracker] (start-ssh-tunnel! (assoc details :host host))
+          tunnel-entrance-port                     (.. tracker getBoundAddress getPort)
+          tunnel-entrance-host                     (.. tracker getBoundAddress getHostName)
+          details-with-tunnel                      (assoc details
+                                                          :port tunnel-entrance-port ;; This parameter is set dynamically when the connection is established
+                                                          :host (str proto "localhost") ;; SSH tunnel will always be through localhost
+                                                          :tunnel-entrance-host tunnel-entrance-host
+                                                          :tunnel-entrance-port tunnel-entrance-port ;; the input port is not known until the connection is opened
+                                                          :tunnel-session session
+                                                          :tunnel-tracker tracker)]
       details-with-tunnel)
     details))
 
+(defn close-tunnel!
+  "Close a running tunnel session"
+  [details]
+  (when (use-ssh-tunnel? details)
+    (.close ^ClientSession (:tunnel-session details))))
+
 (defn with-ssh-tunnel*
   "Starts an SSH tunnel, runs the supplied function with the tunnel open, then closes it"
-  [{:keys [host port tunnel-host tunnel-user tunnel-pass] :as details} f]
+  [details f]
   (if (use-ssh-tunnel? details)
     (let [details-with-tunnel (include-ssh-tunnel details)]
-      (log/errorf "\nbefore:\n%s\n" (with-out-str (clojure.pprint/pprint details)))
-      (log/errorf "\nafter:\n%s\n" (with-out-str (clojure.pprint/pprint details-with-tunnel)))
       (try
-        (log/info (u/format-color 'cyan "<< OPENED SSH TUNNEL >>"))
+        (log/trace (u/format-color 'cyan "<< OPENED SSH TUNNEL >>"))
         (f details-with-tunnel)
         (catch Exception e
           (throw e))
         (finally
-          (.disconnect ^com.jcraft.jsch.Session (:tunnel-connection details-with-tunnel))
-          (log/info (u/format-color 'cyan "<< CLOSED SSH TUNNEL >>")))))
+          (close-tunnel! details-with-tunnel)
+          (log/trace (u/format-color 'cyan "<< CLOSED SSH TUNNEL >>")))))
     (f details)))
 
 (defmacro with-ssh-tunnel
diff --git a/test/metabase/driver/sql_jdbc_test.clj b/test/metabase/driver/sql_jdbc_test.clj
index 54612c80e2c769da635ff89f7799e34d0698da6e..3f45e1a2fee696db477077e40883967fb1d1383e 100644
--- a/test/metabase/driver/sql_jdbc_test.clj
+++ b/test/metabase/driver/sql_jdbc_test.clj
@@ -81,9 +81,9 @@
   (mt/test-driver :postgres
     (testing "Make sure invalid ssh credentials are detected if a direct connection is possible"
       (is (thrown?
-           com.jcraft.jsch.JSchException
+           java.net.ConnectException
+           ;; this test works if sshd is running or not
            (try
-             ;; this test works if sshd is running or not
              (let [details {:dbname         "test"
                             :engine         :postgres
                             :host           "localhost"
@@ -93,18 +93,21 @@
                             :tunnel-enabled true
                             :tunnel-host    "localhost" ; this test works if sshd is running or not
                             :tunnel-pass    "BOGUS-BOGUS-BOGUS"
-                            :tunnel-port    22
+                            ;; we want to use a bogus port here on purpose -
+                            ;; so that locally, it gets a ConnectionRefused,
+                            ;; and in CI it does too. Apache's SSHD library
+                            ;; doesn't wrap every exception in an SshdException
+                            :tunnel-port    21212
                             :tunnel-user    "example"
                             :user           "postgres"}]
                (tu.log/suppress-output
-                 (driver.u/can-connect-with-details? :postgres details :throw-exceptions)))
+                (driver.u/can-connect-with-details? :postgres details :throw-exceptions)))
              (catch Throwable e
                (loop [^Throwable e e]
-                 (or (when (instance? com.jcraft.jsch.JSchException e)
+                 (or (when (instance? java.net.ConnectException e)
                        (throw e))
                      (some-> (.getCause e) recur))))))))))
 
-
 ;;; --------------------------------- Tests for splice-parameters-into-native-query ----------------------------------
 
 (deftest splice-parameters-native-test
diff --git a/test/metabase/util/ssh_test.clj b/test/metabase/util/ssh_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..10cecff04d6cea28bc945c0e76fb55e34f1e3dfe
--- /dev/null
+++ b/test/metabase/util/ssh_test.clj
@@ -0,0 +1,162 @@
+(ns metabase.util.ssh-test
+  (:require [clojure.java.io :as io]
+            [clojure.test :refer :all]
+            [clojure.tools.logging :as log]
+            [metabase.util.ssh :as sshu])
+  (:import (java.io BufferedReader InputStreamReader PrintWriter)
+           (java.net InetSocketAddress ServerSocket Socket)
+           org.apache.sshd.server.forward.AcceptAllForwardingFilter))
+
+(def ^:private ssh-username "jsmith")
+(def ^:private ssh-password "supersecret")
+(def ^:private ssh-publickey "ssh/ssh_test.pub")
+(def ^:private ssh-key "ssh/ssh_test")
+(def ^:private ssh-key-invalid "ssh/ssh_test_invalid")
+(def ^:private ssh-publickey-passphrase "ssh/ssh_test_passphrase.pub")
+(def ^:private ssh-key-with-passphrase "ssh/ssh_test_passphrase")
+(def ^:private ssh-key-passphrase "Password1234")
+(def ^:private ssh-mock-server-with-password-port 12221)
+(def ^:private ssh-mock-server-with-publickey-port 12222)
+(def ^:private ssh-mock-server-with-publickey-passphrase-port 12223)
+
+;;--------------
+;; mock ssh server fixtures
+;;--------------
+
+(defn start-ssh-mock-server-with-password
+  "start a ssh mock server with password auth challenge"
+  []
+  (let [password-auth (reify org.apache.sshd.server.auth.password.PasswordAuthenticator
+                        (authenticate [_ username password session]
+                          (and
+                           (= username ssh-username)
+                           (= password ssh-password))))
+        keypair-provider (new org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider)
+        sshd (doto (org.apache.sshd.server.SshServer/setUpDefaultServer)
+               (.setPort ssh-mock-server-with-password-port)
+               (.setKeyPairProvider keypair-provider)
+               (.setPasswordAuthenticator password-auth))]
+    (log/debug "ssh mock server (with password) started")
+    (.start sshd)
+    sshd))
+
+(defn start-ssh-mock-server-with-publickey
+  "start a ssh mock server with public key auth challenge"
+  [pubkey port]
+  (let [keypair-provider (new org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider)
+        publickey-file (io/file (io/resource pubkey))
+        publickey-auth (new org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator
+                            (.toPath publickey-file))
+        sshd (doto (org.apache.sshd.server.SshServer/setUpDefaultServer)
+               (.setPort port)
+               (.setKeyPairProvider keypair-provider)
+               (.setPublickeyAuthenticator publickey-auth)
+               (.setForwardingFilter AcceptAllForwardingFilter/INSTANCE))]
+    (log/debug "ssh mock server (with publickey) started")
+    (.start sshd)
+    sshd))
+
+(use-fixtures :once
+  (fn [f]
+    (let [servers [(start-ssh-mock-server-with-password)
+                   (start-ssh-mock-server-with-publickey ssh-publickey ssh-mock-server-with-publickey-port)
+                   (start-ssh-mock-server-with-publickey ssh-publickey-passphrase ssh-mock-server-with-publickey-passphrase-port)]]
+      (try (f)
+           (finally
+             (doseq [server servers]
+               (try
+                 (when server
+                   (.stop server))
+                 (catch Exception e
+                   (log/error e)))))))))
+
+;;--------------
+;; tests
+;;--------------
+
+;; correct password
+(deftest connects-with-correct-password
+  (sshu/start-ssh-tunnel!
+   {:tunnel-user ssh-username
+    :tunnel-host "127.0.0.1"
+    :tunnel-port ssh-mock-server-with-password-port
+    :tunnel-pass ssh-password
+    :host "127.0.0.1"
+    :port 1234}))
+
+;; incorrect password
+(deftest throws-exception-on-incorrect-password
+  (is (thrown? org.apache.sshd.common.SshException
+               (sshu/start-ssh-tunnel!
+                {:tunnel-user ssh-username
+                 :tunnel-host "127.0.0.1"
+                 :tunnel-port ssh-mock-server-with-password-port
+                 :tunnel-pass (str ssh-password "invalid")
+                 :host "127.0.0.1"
+                 :port 1234}))))
+
+;; correct ssh key
+(deftest connects-with-correct-ssh-key
+  (sshu/start-ssh-tunnel!
+   {:tunnel-user ssh-username
+    :tunnel-host "127.0.0.1"
+    :tunnel-port ssh-mock-server-with-publickey-port
+    :tunnel-private-key (slurp (io/resource ssh-key))
+    :host "127.0.0.1"
+    :port 1234}))
+
+;; incorrect ssh key
+(deftest throws-exception-on-incorrect-ssh-key
+  (is (thrown? org.apache.sshd.common.SshException
+               (sshu/start-ssh-tunnel!
+                {:tunnel-user ssh-username
+                 :tunnel-host "127.0.0.1"
+                 :tunnel-port ssh-mock-server-with-publickey-port
+                 :tunnel-private-key (slurp (io/resource ssh-key-invalid))
+                 :host "127.0.0.1"
+                 :port 1234}))))
+
+;; correct ssh key
+(deftest connects-with-correct-ssh-key-and-passphrase
+  (sshu/start-ssh-tunnel!
+   {:tunnel-user ssh-username
+    :tunnel-host "127.0.0.1"
+    :tunnel-port ssh-mock-server-with-publickey-passphrase-port
+    :tunnel-private-key (slurp (io/resource ssh-key-with-passphrase))
+    :tunnel-private-key-passphrase ssh-key-passphrase
+    :host "127.0.0.1"
+    :port 1234}))
+
+(deftest throws-exception-on-incorrect-ssh-key-and-passphrase
+  (is (thrown? java.io.StreamCorruptedException
+               (sshu/start-ssh-tunnel!
+                {:tunnel-user ssh-username
+                 :tunnel-host "127.0.0.1"
+                 :tunnel-port ssh-mock-server-with-publickey-passphrase-port
+                 :tunnel-private-key (slurp (io/resource ssh-key-with-passphrase))
+                 :tunnel-private-key-passphrase "this-is-the-wrong-passphrase"
+                 :host "127.0.0.1"
+                 :port 1234}))))
+
+(deftest ssh-tunnel-works
+  (testing "ssh tunnel can properly tunnel"
+    ;; this will try to open a TCP connection via the tunnel. If it fails,
+    (sshu/with-ssh-tunnel [details-with-tunnel {:tunnel-enabled true
+                                                :tunnel-user ssh-username
+                                                :tunnel-host "127.0.0.1"
+                                                :tunnel-port ssh-mock-server-with-publickey-passphrase-port
+                                                :tunnel-private-key (slurp (io/resource ssh-key-with-passphrase))
+                                                :tunnel-private-key-passphrase ssh-key-passphrase
+                                                :host "127.0.0.1"
+                                                :port 41414}]
+      (with-open [server (doto (ServerSocket. 41414)
+                           (.setSoTimeout 10000))
+                  socket (Socket.)]
+        (let [server-thread (future (with-open [client-socket (.accept server)
+                                                out-server (PrintWriter. (.getOutputStream client-socket) true)]
+                                      (.println out-server "hello from the ssh tunnel")))]
+          (.connect socket (InetSocketAddress. "127.0.0.1" (:tunnel-entrance-port details-with-tunnel)) 3000)
+          ;; cause our future to run to completion
+          @server-thread
+          (with-open [in-client (BufferedReader. (InputStreamReader. (.getInputStream socket)))]
+            (is (= "hello from the ssh tunnel" (.readLine in-client)))))))))
diff --git a/test_resources/ssh/README.md b/test_resources/ssh/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..aa5beaabe860f24a63be72c68496254653cb18de
--- /dev/null
+++ b/test_resources/ssh/README.md
@@ -0,0 +1,7 @@
+# ssh test keys
+
+Use this command to generate these keys:
+
+```
+$ ssh-keygen -t rsa -m PEM
+```
diff --git a/test_resources/ssh/ssh_test b/test_resources/ssh/ssh_test
new file mode 100644
index 0000000000000000000000000000000000000000..b7653ef38c4e3fdb7f5c46c41384a7e31a68fac1
--- /dev/null
+++ b/test_resources/ssh/ssh_test
@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/test_resources/ssh/ssh_test.pub b/test_resources/ssh/ssh_test.pub
new file mode 100644
index 0000000000000000000000000000000000000000..a997410bbba2462bc14f673d6ae3ddbae9865c38
--- /dev/null
+++ b/test_resources/ssh/ssh_test.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGOIPsjFweWcVxTQfAx6cZc2XN+wLdrd1KDrP1MipwYgWE0V51/U+d8ScDPjUGYltabxKXEOMQaVxlDpedIPVN51glKA5L9IKtNO8om2eX3xNXUJWlHnPtA0GQGSS0EfMWtkdipOX+AikzIRPzik91FsKDsxnZD79ZrtyDo39INEQxJigpcv6xpD8f3n9o1F+QgsQekaMtIZwo43pSaVJZnpaoaDORk9/0FI8Keq9vMxjlvPFdUgC6Ib3CO5CSgAH2Ev9DCNi3q3apLsPxeTHoguYqv86ujA9tMwanWmc6HWDvbwM2g9497s2k3gPT6RzPsgd6f/9ZhZPegeaLF4gOEza5NgLwJ0oKWnZyNWoZEpPU0LS4uXRc+dBVVNF8J569q9uyl2OUkd/ICqx0P+rzx2A/1FUZwxgyry4lcCvLxJCmFFrt4m6/dlhvuyszip6gDfb1K8cAPQADnZoflaDfGmU3XpIetKnwKs47+eQIvDTzr/+HHv24E2HNbzkFYQM=
diff --git a/test_resources/ssh/ssh_test_invalid b/test_resources/ssh/ssh_test_invalid
new file mode 100644
index 0000000000000000000000000000000000000000..15845157488cdd0889b1bd05d796d2121baf1a58
--- /dev/null
+++ b/test_resources/ssh/ssh_test_invalid
@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/test_resources/ssh/ssh_test_invalid.pub b/test_resources/ssh/ssh_test_invalid.pub
new file mode 100644
index 0000000000000000000000000000000000000000..3a5715bba75da503e806417b28b500d0615ddd4c
--- /dev/null
+++ b/test_resources/ssh/ssh_test_invalid.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAFKKaiSiicpURjxbaTmk27s7iwZGs2GYRDXeO0BI8jJHha4akh/VN5cP+9r0DwlPsiqnPfGVVzKS9s3SnFxNUxexBAu2tVmYRsEnd418pUb/EhJkpAM9XlgzgWG02HKGXyotDHIJOcloTW1Am9XQv3qbAInhObBfdDjtBMm4LquR0TBbeBilQTrt6csQ7DfQQhOhLZ6V6HNlSb/M66MC89ZPRdV3FwWeodBC8Hp162oQE/pfDyoKtIav2O5WHoalJcpnXZ8HEktQFn1c7nOOYb0BUDfC0+hBMhyORmDL30xEbAo3fs4h3j5bENFTQA5DFrBZ68BX7ie6OqC2foKNJb89okToBA93GDcCjcnOsQRTH7N324P6aaz50vD2sqJS4X0x2XA7XMoZ96EBM2U+jomQ8aiUoDgbiQI5kSwa0nEERJ8aXGNzm3wO5JwnanTj9MM6WfftDsB69SxBH203oY3BF+FKD0xlwj30z+fhpSqGW9ZLWnhlYhWkFQRM1WP8=
diff --git a/test_resources/ssh/ssh_test_passphrase b/test_resources/ssh/ssh_test_passphrase
new file mode 100644
index 0000000000000000000000000000000000000000..49530610bba07751171a6c0eb96f634f8d16d181
--- /dev/null
+++ b/test_resources/ssh/ssh_test_passphrase
@@ -0,0 +1,39 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+Wwh5+ivBXeXSoE099eFwO7dMERI=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test_resources/ssh/ssh_test_passphrase.pub b/test_resources/ssh/ssh_test_passphrase.pub
new file mode 100644
index 0000000000000000000000000000000000000000..6ab3b3bd2c357713f8851474db24c045d17ec63f
--- /dev/null
+++ b/test_resources/ssh/ssh_test_passphrase.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDrB4H3qCaYrNt1P+mDrF23+NJ5M6povEzFAM/TcFm0kT0mMhqoBzFGu0ELUbew1xdoH0kbkpo2b7ur4M+tjC96196Z4Soa1N7M4VNokrgmb0f0iMV/rsPeEIEJgPQBn4YAepWtLUCeaZQ5TOzImwUjVeuvMzAzRDowJPDyM1POiY/JibMI/UCiZZny5W5qLTvhJSiyTNuiC+RjdSkFN2heEJyE0r4LN4Psk5wUXxdWDgIw5H978y7MDZNdPzTl8t+X8nARYLoXnE1ne4lHXBsZg06UUPf/IhPmoxp4CI1j8W+jNbDg0+3ceLQ2etVa4Vjj68cE8giaXfFMcqpeIh0M8SegomR89lNGwsPBdnQMrNi5Oxg5avAQo41Rq/MkK8YKDjbF4I+UpKCr8W280EYhC+b+tftynryMrZ2WDcTmoCyAhh9ltb7ab7GCxKgBs3t5+l9b/8GJf1vPBtYb45zVjy71+ppAy/sFt/NoaLx9CV6KKEoQnw2dWmYi0P9lfcU= rroland@carter