diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj
index 4bc9db68ddcee81b7f825a964fba29a8fc2ef793..1262ff2a1ff69f524a90bf8f7588c05ff1c17126 100644
--- a/src/metabase/models/database.clj
+++ b/src/metabase/models/database.clj
@@ -1,6 +1,7 @@
 (ns metabase.models.database
   (:require [cheshire.generate :refer [add-encoder encode-map]]
             [clojure.tools.logging :as log]
+            [java-time :as t]
             [medley.core :as m]
             [metabase.api.common :refer [*current-user*]]
             [metabase.db.util :as mdb.u]
@@ -9,6 +10,7 @@
             [metabase.models.interface :as i]
             [metabase.models.permissions :as perms]
             [metabase.models.permissions-group :as perm-group]
+            [metabase.models.secret :as secret]
             [metabase.plugins.classloader :as classloader]
             [metabase.util :as u]
             [metabase.util.i18n :refer [trs]]
@@ -69,10 +71,65 @@
     (catch Throwable e
       (log/error e (trs "Error sending database deletion notification")))))
 
-;; TODO - this logic would make more sense in post-update if such a method existed
+(defn- handle-db-details-secret-prop!
+  "Helper fn for reducing over a map of all the secret connection-properties, keyed by name. This is side effecting. At
+  each iteration step, if there is a -value suffixed property set in the details to be persisted, then we instead insert
+  (or update an existing) Secret instance and point to the inserted -id instead."
+  [database details conn-prop-nm conn-prop]
+  (let [sub-prop  (fn [suffix]
+                    (keyword (str conn-prop-nm suffix)))
+        id-kw     (sub-prop "-id")
+        new-name  (format "%s for %s" (:display-name conn-prop) (:name database))
+        ;; in the future, when secret values can simply be changed by passing
+        ;; in a new ID (as opposed to a new value), this behavior will change,
+        ;; but for now, we should simply look for the value
+        path-kw   (sub-prop "-path")
+        value-kw  (sub-prop "-value")
+        value     (if-let [v (value-kw details)]     ; the -value suffix was specified; use that
+                    v
+                    (when-let [path (path-kw details)] ; the -path suffix was specified; this is actually a :file-path
+                      path))
+        kind      (:secret-kind conn-prop)
+        source    (when (path-kw details)
+                    :file-path)]                     ; set the :source due to the -path suffix (see above)
+    (if (nil? value) ;; secret value for this conn prop was not changed
+      details
+      (let [{:keys [id creator_id created_at]} (secret/upsert-secret-value!
+                                                 (id-kw details)
+                                                 new-name
+                                                 kind
+                                                 source
+                                                 value)]
+        ;; remove the -value keyword (since in the persisted details blob, we only ever want to store the -id)
+        (-> details
+          (dissoc value-kw)
+          (assoc id-kw id)
+          (assoc (sub-prop "-source") source)
+          (assoc (sub-prop "-creator-id") creator_id)
+          (assoc (sub-prop "-created-at") (t/format :iso-offset-date-time created_at)))))))
+
+(defn- handle-secrets-changes [{:keys [details] :as database}]
+  (let [conn-props-fn (get-method driver/connection-properties (driver.u/database->driver database))]
+    (cond (nil? conn-props-fn)
+          database ; no connection-properties multimethod defined; can't check secret types so do nothing
+
+          details ; we have details populated in this Database instance, so handle them
+          (let [conn-props            (conn-props-fn (driver.u/database->driver database))
+                conn-secrets-by-name  (->> (filter #(= "secret" (:type %)) conn-props)
+                                           (reduce (fn [acc prop] (assoc acc (:name prop) prop)) {}))
+                ;; saved-details         (db/select-field :details Database id)
+                 updated-details      (reduce-kv (partial handle-db-details-secret-prop! database)
+                                                 details
+                                                 conn-secrets-by-name)]
+           (assoc database :details updated-details))
+
+          :else ; no details populated; do nothing
+          database)))
+
 (defn- pre-update
   [{new-metadata-schedule :metadata_sync_schedule, new-fieldvalues-schedule :cache_field_values_schedule, :as database}]
-  (u/prog1 database
+  (u/prog1 (handle-secrets-changes database)
+    ;; TODO - this logic would make more sense in post-update if such a method existed
     ;; if the sync operation schedules have changed, we need to reschedule this DB
     (when (or new-metadata-schedule new-fieldvalues-schedule)
       (let [{old-metadata-schedule    :metadata_sync_schedule
@@ -101,6 +158,8 @@
              :metadata_sync_schedule      new-metadata-schedule
              :cache_field_values_schedule new-fieldvalues-schedule)))))))
 
+(defn- pre-insert [database]
+  (handle-secrets-changes database))
 
 (defn- perms-objects-set [database _]
   #{(perms/data-perms-path (u/the-id database))})
@@ -118,6 +177,7 @@
           :properties     (constantly {:timestamped? true})
           :post-insert    post-insert
           :post-select    post-select
+          :pre-insert     pre-insert
           :pre-update     pre-update
           :pre-delete     pre-delete})
   i/IObjectPermissions
diff --git a/src/metabase/models/secret.clj b/src/metabase/models/secret.clj
index 0d3a44468ad436d77b653c1094812bfed9239003..c4d742a3ec7f72fac8e47af08bf7686d541b1755 100644
--- a/src/metabase/models/secret.clj
+++ b/src/metabase/models/secret.clj
@@ -1,7 +1,9 @@
 (ns metabase.models.secret
   (:require [cheshire.generate :refer [add-encoder encode-map]]
+            [metabase.api.common :as api]
             [metabase.models.interface :as i]
             [metabase.util :as u]
+            [toucan.db :as db]
             [toucan.models :as models]))
 
 ;;; ----------------------------------------------- Entity & Lifecycle -----------------------------------------------
@@ -25,6 +27,46 @@
 ;;; ---------------------------------------------- Hydration / Util Fns ----------------------------------------------
 ;; none yet
 
+(def
+  ^{:doc "The attributes of a secret which, if changed, will result in a version bump" :private true}
+  bump-version-keys
+  [:kind :source :value])
+
+(defn latest-for-id
+  "Returns the latest Secret instance for the given `id` (meaning the one with the highest `version`)."
+  {:added "0.41.0"}
+  [id]
+  (db/select-one Secret :id id {:order-by [[:version :desc]]}))
+
+(defn upsert-secret-value!
+  "Inserts a new secret value, or updates an existing one, for the given parameters.
+   * if there is no existing Secret instance, inserts with the given field values
+   * if there is an existing latest Secret instance, and the value (or any of the supporting fields, like kind or
+       source) has changed, then inserts a new version with the given parameters.
+   * if there is an existing latest Secret instance, but none of the aforementioned fields changed, then update it"
+  {:added "0.41.0"}
+  [existing-id nm kind source value]
+  (let [insert-new     (fn [id v]
+                         (let [inserted (db/insert! Secret (cond-> {:version    v
+                                                                    :name       nm
+                                                                    :kind       kind
+                                                                    :source     source
+                                                                    :value      value
+                                                                    :creator_id api/*current-user-id*}
+                                                             id
+                                                             (assoc :id id)))]
+                           ;; Toucan doesn't support composite primary keys, so adding a new record with incremented
+                           ;; version for an existing ID won't return a result from db/insert!, hence we may need to
+                           ;; manually select it here
+                           (or inserted (db/select-one Secret :id id :version v))))
+        latest-version (when existing-id (latest-for-id existing-id))]
+    (if latest-version
+      (if (= (select-keys latest-version bump-version-keys) [kind source value])
+        (db/update-where! Secret {:id existing-id :version (:version latest-version)}
+                                 :name nm)
+        (insert-new (u/the-id latest-version) (inc (:version latest-version))))
+      (insert-new nil 1))))
+
 ;;; -------------------------------------------------- JSON Encoder --------------------------------------------------
 
 (add-encoder SecretInstance (fn [secret json-generator]
diff --git a/test/metabase/models/database_test.clj b/test/metabase/models/database_test.clj
index dab9668e3b8b32ea8933910e48ca9f94f4d17500..ed55108e33b6a15a03a4acfd7172e4811016065f 100644
--- a/test/metabase/models/database_test.clj
+++ b/test/metabase/models/database_test.clj
@@ -2,19 +2,24 @@
   (:require [cheshire.core :refer [decode encode]]
             [clojure.string :as str]
             [clojure.test :refer :all]
+            [metabase.api.common :as api]
             [metabase.driver :as driver]
             [metabase.driver.util :as driver.u]
             [metabase.models :refer [Database]]
             [metabase.models.database :as mdb]
             [metabase.models.permissions :as perms]
+            [metabase.models.secret :as secret]
             [metabase.models.user :as user]
             [metabase.server.middleware.session :as mw.session]
             [metabase.task :as task]
             [metabase.task.sync-databases :as task.sync-databases]
             [metabase.test :as mt]
+            [metabase.test.fixtures :as fixtures]
             [schema.core :as s]
             [toucan.db :as db]))
 
+(use-fixtures :once (fixtures/initialize :db :plugins :test-drivers))
+
 (defn- trigger-for-db [db-id]
   (some (fn [{trigger-key :key, :as trigger}]
           (when (str/ends-with? trigger-key (str \. db-id))
@@ -155,3 +160,44 @@
            (mdb/sensitive-fields-for-db nil)))
     (is (= driver.u/default-sensitive-fields
            (mdb/sensitive-fields-for-db {})))))
+
+(deftest secret-resolution-test
+  (mt/with-driver :secret-test-driver
+    (binding [api/*current-user-id* (mt/user->id :crowberto)]
+      (letfn [(check-db-fn [{:keys [details] :as database} exp-secret]
+                (is (not (contains? details :password-value)) "password-value was removed from details")
+                (is (some? (:password-created-at details)) "password-created-at was populated in details")
+                (is (= (mt/user->id :crowberto) (:password-creator-id details))
+                    "password-creator-id was populated in details")
+                (is (= (if-let [src (:source exp-secret)]
+                         (name src)
+                         nil) (:password-source details)) "password-source matches the value from the secret")
+                (is (contains? details :password-id) "password-id was added to details")
+                (let [{:keys [created_at updated_at] :as secret} (secret/latest-for-id (:password-id details))]
+                  (is (some? secret) "Loaded Secret instance by ID")
+                  (is (some? created_at) "created_at populated for the secret instance")
+                  (is (some? updated_at) "updated_at populated for the secret instance")
+                  (doseq [[exp-key exp-val] exp-secret]
+                    (testing (format "%s=%s in secret" exp-key exp-val)
+                      (is (= exp-val (cond-> (exp-key secret)
+                                       (string? exp-val)
+                                       (String.)
+
+                                       :else
+                                       identity)))))))]
+        (testing "values for referenced secret IDs are resolved in a new DB"
+          (mt/with-temp Database [{:keys [id details] :as database} {:engine  :secret-test-driver
+                                                                     :name    "Test DB with secrets"
+                                                                     :details {:host           "localhost"
+                                                                               :password-value "new-password"}}]
+            (testing " and saved db-details looks correct"
+              (check-db-fn database {:kind    :password
+                                     :source  nil
+                                     :version 1
+                                     :value   "new-password"})
+              (testing " updating the value works as expected"
+                (db/update! Database id :details (assoc details :password-path  "/path/to/my/password-file"))
+                (check-db-fn (Database id) {:kind    :password
+                                            :source  :file-path
+                                            :version 2
+                                            :value   "/path/to/my/password-file"})))))))))
diff --git a/test/metabase/test/initialize.clj b/test/metabase/test/initialize.clj
index bf14f7e9983eae380b26becb894afb2b393770e2..0c67488227ceec74e43d81d5d286bbde322df664 100644
--- a/test/metabase/test/initialize.clj
+++ b/test/metabase/test/initialize.clj
@@ -90,7 +90,7 @@
 (define-initialization :test-drivers
   (classloader/require 'metabase.test.initialize.plugins)
   ((resolve 'metabase.test.initialize.plugins/init-test-drivers!)
-   [:driver-deprecation-test-legacy :driver-deprecation-test-new]))
+   [:driver-deprecation-test-legacy :driver-deprecation-test-new :secret-test-driver]))
 
 ;; initializing the DB also does setup needed so the scheduler will work correctly. (Remember that the scheduler uses
 ;; a JDBC backend!)
diff --git a/test_modules/drivers/secret-test-driver/resources/metabase-plugin.yaml b/test_modules/drivers/secret-test-driver/resources/metabase-plugin.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b65a491c52097ed699cb9a3c3f2a4cc30120906e
--- /dev/null
+++ b/test_modules/drivers/secret-test-driver/resources/metabase-plugin.yaml
@@ -0,0 +1,19 @@
+info:
+  name: Secret Test Driver
+  version: 0.0.0-SNAPSHOT
+  description: Test driver for secrets
+driver:
+  name: secret-test-driver
+  display-name: Secret Test Driver
+  lazy-load: true
+  parent: sql
+  connection-properties:
+    - name: host
+      display-name: Host
+      placeholder: localhost
+    - name: password
+      display-name: Password
+      type: secret
+      secret-kind: password
+      placeholder: foo
+      required: true