diff --git a/src/metabase/api/setting.clj b/src/metabase/api/setting.clj
index ab173a82628eee5f998095e923992a7f1d56ea21..5e87b2b4c49494ac081ca31555d1995c09c9620b 100644
--- a/src/metabase/api/setting.clj
+++ b/src/metabase/api/setting.clj
@@ -22,11 +22,7 @@
   [key]
   {key su/NonBlankString}
   (api/check-superuser)
-  (let [k (keyword key)
-        v (setting/get k)]
-    ;; for security purposes, don't return value of a setting if it was defined via env var
-    (when (not= v (setting/env-var-value k))
-      v)))
+  (setting/user-facing-value key))
 
 (api/defendpoint PUT "/:key"
   "Create/update a `Setting`. You must be a superuser to do this.
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 2f37d7f67527fe29a15b19a4f98631a8175934f7..72fd5395526dea8e4e04f37efb65af29451794c9 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -12,13 +12,25 @@
   (:import javax.mail.Session))
 
 ;;; CONFIG
+
+(defsetting email-from-address
+  (tru "Email address you want to use as the sender of Metabase.")
+  :default "notifications@metabase.com")
+
+(defsetting email-smtp-host
+  (tru "The address of the SMTP server that handles your emails."))
+
+(defsetting email-smtp-username
+  (tru "SMTP username."))
+
+(defsetting email-smtp-password
+  (tru "SMTP password.")
+  :sensitive? true)
+
 ;; TODO - smtp-port should be switched to type :integer
+(defsetting email-smtp-port
+  (tru "The port your SMTP server uses for outgoing emails."))
 
-(defsetting email-from-address  (tru "Email address you want to use as the sender of Metabase.") :default "notifications@metabase.com")
-(defsetting email-smtp-host     (tru "The address of the SMTP server that handles your emails."))
-(defsetting email-smtp-username (tru "SMTP username."))
-(defsetting email-smtp-password (tru "SMTP password."))
-(defsetting email-smtp-port     (tru "The port your SMTP server uses for outgoing emails."))
 (defsetting email-smtp-security
   (tru "SMTP secure connection protocol. (tls, ssl, starttls, or none)")
   :default (tru "none")
diff --git a/src/metabase/integrations/ldap.clj b/src/metabase/integrations/ldap.clj
index e2a9992b8edd4996e6b0b19b2ab280969d7a1bc2..89453b4844690de0777325b56b636e021f48a3f0 100644
--- a/src/metabase/integrations/ldap.clj
+++ b/src/metabase/integrations/ldap.clj
@@ -39,7 +39,8 @@
   (tru "The Distinguished Name to bind as (if any), this user will be used to lookup information about other users."))
 
 (defsetting ldap-password
-  (tru "The password to bind with for the lookup user."))
+  (tru "The password to bind with for the lookup user.")
+  :sensitive? true)
 
 (defsetting ldap-user-base
   (tru "Search base for users. (Will be searched recursively)"))
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 2b7213bd0bace7e7e240f3378e832b8bc6392924..7079d7de9e84626fed64e62ed08bce0b178ddb39 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -33,18 +33,14 @@
             [clojure
              [core :as core]
              [string :as str]]
-            [clojure.core.memoize :as memoize]
-            [clojure.java.jdbc :as jdbc]
             [clojure.tools.logging :as log]
             [environ.core :as env]
-            [honeysql.core :as hsql]
             [metabase
-             [db :as mdb]
              [events :as events]
              [util :as u]]
+            [metabase.models.setting.cache :as cache]
             [metabase.util
              [date :as du]
-             [honeysql-extensions :as hx]
              [i18n :as ui18n :refer [trs tru]]]
             [schema.core :as s]
             [toucan
@@ -81,6 +77,7 @@
    :getter      clojure.lang.IFn ; different getters/setters take care of parsing/unparsing
    :setter      clojure.lang.IFn
    :tag         (s/maybe Class)  ; type annotation, e.g. ^String, to be applied. Defaults to tag based on :type
+   :sensitive?  s/Bool           ; is this sensitive (never show in plaintext), like a password? (default: false)
    :internal?   s/Bool           ; should the API never return this setting? (default: false)
    :cache?      s/Bool})         ; should the getter always fetch this value "fresh" from the DB? (default: false)
 
@@ -98,128 +95,6 @@
                   (str (tru "Setting {0} does not exist.\nFound: {1}" k (sort (keys @registered-settings))))))))))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                                     cache                                                      |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-;; Cache is a 1:1 mapping of what's in the DB
-;; Cached lookup time is ~60µs, compared to ~1800µs for DB lookup
-
-(def ^:private cache
-  "Settings cache. Map of Setting key (string) -> Setting value (string)."
-  (atom nil))
-
-;; CACHE SYNCHRONIZATION
-;;
-;; When running multiple Metabase instances (horizontal scaling), it is of course possible for one instance to update
-;; a Setting, and, since Settings are cached (to avoid tons of DB calls), for the other instances to then have an
-;; out-of-date cache. Thus we need a way for instances to know when their caches are out of date, so they can update
-;; them accordingly. Here is our solution:
-;;
-;; We will record the last time *any* Setting was updated in a special Setting called `settings-last-updated`.
-;;
-;; Since `settings-last-updated` itself is a Setting, it will get fetched as part of each instance's local cache; we
-;; can then periodically compare the locally cached value of `settings-last-updated` with the value in the DB. If our
-;; locally cached value is older than the one in the DB, we will flush our cache. When the cache is fetched again, it
-;; will have the up-to-date value.
-;;
-;; Because different machines can have out-of-sync clocks, we'll rely entirely on the application DB for caclulating
-;; and comparing values of `settings-last-updated`. Because the Setting table itself only stores text values, we'll
-;; need to cast it between TEXT and TIMESTAMP SQL types as needed.
-
-(def ^:private ^String settings-last-updated-key "settings-last-updated")
-
-(defn- update-settings-last-updated!
-  "Update the value of `settings-last-updated` in the DB; if the row does not exist, insert one."
-  []
-  (log/debug (trs "Updating value of settings-last-updated in DB..."))
-  ;; for MySQL, cast(current_timestamp AS char); for H2 & Postgres, cast(current_timestamp AS text)
-  (let [current-timestamp-as-string-honeysql (hx/cast (if (= (mdb/db-type) :mysql) :char :text)
-                                                      (hsql/raw "current_timestamp"))]
-    ;; attempt to UPDATE the existing row. If no row exists, `update-where!` will return false...
-    (or (db/update-where! Setting {:key settings-last-updated-key} :value current-timestamp-as-string-honeysql)
-        ;; ...at which point we will try to INSERT a new row. Note that it is entirely possible two instances can both
-        ;; try to INSERT it at the same time; one instance would fail because it would violate the PK constraint on
-        ;; `key`, and throw a SQLException. As long as one instance updates the value, we are fine, so we can go ahead
-        ;; and ignore that Exception if one is thrown.
-        (try
-          ;; Use `simple-insert!` because we do *not* want to trigger pre-insert behavior, such as encrypting `:value`
-          (db/simple-insert! Setting :key settings-last-updated-key, :value current-timestamp-as-string-honeysql)
-          (catch java.sql.SQLException e
-            ;; go ahead and log the Exception anyway on the off chance that it *wasn't* just a race condition issue
-            (log/error (trs "Error inserting a new Setting: {0}"
-                            (with-out-str (jdbc/print-sql-exception-chain e))))))))
-  ;; Now that we updated the value in the DB, go ahead and update our cached value as well, because we know about the
-  ;; changes
-  (swap! cache assoc settings-last-updated-key (db/select-one-field :value Setting :key settings-last-updated-key)))
-
-(defn- cache-out-of-date?
-  "Check whether our Settings cache is out of date. We know the cache is out of date if either of the following
-  conditions is true:
-
-   *  The cache is empty (the `cache` atom is `nil`), which of course means it needs to be updated
-   *  There is a value of `settings-last-updated` in the cache, and it is older than the value of in the DB. (There
-      will be no value until the first time a normal Setting is updated; thus if it is not yet set, we do not yet need
-      to invalidate our cache.)"
-  []
-  (log/debug (trs "Checking whether settings cache is out of date (requires DB call)..."))
-  (boolean
-   (or
-    ;; is the cache empty?
-    (not @cache)
-    ;; if not, get the cached value of `settings-last-updated`, and if it exists...
-    (when-let [last-known-update (core/get @cache settings-last-updated-key)]
-      ;; compare it to the value in the DB. This is done be seeing whether a row exists
-      ;; WHERE value > <local-value>
-      (u/prog1 (db/select-one Setting
-                 {:where [:and
-                          [:= :key settings-last-updated-key]
-                          [:> :value last-known-update]]})
-        (when <>
-          (log/info (u/format-color 'red
-                        (str (trs "Settings have been changed on another instance, and will be reloaded here."))))))))))
-
-(def ^:private cache-update-check-interval-ms
-  "How often we should check whether the Settings cache is out of date (which requires a DB call)?"
-  ;; once a minute
-  (* 60 1000))
-
-(def ^:private ^{:arglists '([])} should-restore-cache?
-  "TTL-memoized version of `cache-out-of-date?`. Call this function to see whether we need to repopulate the cache with
-  values from the DB."
-  (memoize/ttl cache-out-of-date? :ttl/threshold cache-update-check-interval-ms))
-
-(def ^:private restore-cache-if-needed-lock (Object.))
-
-(defn- restore-cache-if-needed!
-  "Check whether we need to repopulate the cache with fresh values from the DB (because the cache is either empty or
-  known to be out-of-date), and do so if needed. This is intended to be called every time a Setting value is
-  retrieved, so it should be efficient; thus the calculation (`should-restore-cache?`) is itself TTL-memoized."
-  []
-  ;; There's a potential race condition here where two threads both call this at the exact same moment, and both get
-  ;; `true` when they call `should-restore-cache`, and then both simultaneously try to update the cache (or, one
-  ;; updates the cache, but the other calls `should-restore-cache?` and gets `true` before the other calls
-  ;; `memo-swap!` (see below))
-  ;;
-  ;; This is not desirable, since either situation would result in duplicate work. Better to just add a quick lock
-  ;; here so only one of them does it, since at any rate waiting for the other thread to finish the task in progress is
-  ;; certainly quicker than starting the task ourselves from scratch
-  (locking restore-cache-if-needed-lock
-    (when (should-restore-cache?)
-      (log/debug (trs "Refreshing Settings cache..."))
-      (reset! cache (db/select-field->field :key :value Setting))
-      ;; Now the cache is up-to-date. That is all good, but if we call `should-restore-cache?` again in a second it
-      ;; will still return `true`, because its result is memoized, and we would be on the hook to (again) update the
-      ;; cache. So go ahead and clear the memozied results for `should-restore-cache?`. The next time around when
-      ;; someone calls this it will cache the latest value (which should be `false`)
-      ;;
-      ;; NOTE: I tried using `memo-swap!` instead to set the cached response to `false` here, avoiding the extra DB
-      ;; call the next fn call would make, but it didn't seem to work correctly (I think it was still discarding the
-      ;; new value because of the TTL). So we will just stick with `memo-clear!` for now. (One extra DB call whenever
-      ;; the cache gets invalidated shouldn't be a huge deal)
-      (memoize/memo-clear! should-restore-cache?))))
-
-
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                                      get                                                       |
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -252,8 +127,8 @@
   (if *disable-cache*
     (db/select-one-field :value Setting :key (setting-name setting-or-name))
     (do
-      (restore-cache-if-needed!)
-      (clojure.core/get @cache (setting-name setting-or-name)))))
+      (cache/restore-cache-if-needed!)
+      (clojure.core/get (cache/cache) (setting-name setting-or-name)))))
 
 
 (defn get-string
@@ -336,7 +211,7 @@
 (defn- update-setting!
   "Update an existing Setting. Used internally by `set-string!` below; do not use directly."
   [setting-name new-value]
-  (assert (not= setting-name settings-last-updated-key)
+  (assert (not= setting-name cache/settings-last-updated-key)
     (tru "You cannot update `settings-last-updated` yourself! This is done automatically."))
   ;; This is indeed a very annoying way of having to do things, but `update-where!` doesn't call `pre-update` (in case
   ;; it updates thousands of objects). So we need to manually trigger `pre-update` behavior by calling `do-pre-update`
@@ -361,34 +236,42 @@
                    (tru "Assuming Setting already exists in DB and updating existing value."))
          (update-setting! setting-name new-value))))
 
+(defn- obfuscated-value? [v]
+  (when (seq v)
+    (boolean (re-matches #"^\*{10}.{2}$" v))))
+
 (s/defn set-string!
   "Set string value of `setting-or-name`. A `nil` or empty `new-value` can be passed to unset (i.e., delete)
   `setting-or-name`. String-type settings use this function directly; all other types ultimately call this (e.g.
   `set-boolean!` eventually calls `set-string!`). Returns the `new-value`."
   [setting-or-name, new-value :- (s/maybe s/Str)]
-  (let [new-value    (when (seq new-value)
-                       new-value)
-        setting      (resolve-setting setting-or-name)
-        setting-name (setting-name setting)]
-    (restore-cache-if-needed!)
-    ;; write to DB
-    (cond
-      (not new-value)                 (db/simple-delete! Setting :key setting-name)
-      ;; if there's a value in the cache then the row already exists in the DB; update that
-      (contains? @cache setting-name) (update-setting! setting-name new-value)
-      ;; if there's nothing in the cache then the row doesn't exist, insert a new one
-      :else                           (set-new-setting! setting-name new-value))
-    ;; update cached value
-    (if new-value
-      (swap! cache assoc  setting-name new-value)
-      (swap! cache dissoc setting-name))
-    ;; Record the fact that a Setting has been updated so eventaully other instances (if applicable) find out about it
-    ;; (For Settings that don't use the Cache, don't update the `last-updated` value, because it will cause other
-    ;; instances to do needless reloading of the cache from the DB)
-    (when-not *disable-cache*
-      (update-settings-last-updated!))
-    ;; Now return the `new-value`.
-    new-value))
+  (let [new-value                         (when (seq new-value)
+                                            new-value)
+        {:keys [sensitive?], :as setting} (resolve-setting setting-or-name)
+        obfuscated?                       (and sensitive? (obfuscated-value? new-value))
+        setting-name                      (setting-name setting)]
+    ;; if someone attempts to set a sensitive setting to an obfuscated value (probably via a misuse of the `set-many!`
+    ;; function, setting values that have not changed), ignore the change. Log a message that we are ignoring it.
+    (if obfuscated?
+      (log/info (trs "Attempted to set Setting {0} to obfuscated value. Ignoring change." setting-name))
+      (do
+        (cache/restore-cache-if-needed!)
+        ;; write to DB
+        (cond
+          (nil? new-value)                       (db/simple-delete! Setting :key setting-name)
+          ;; if there's a value in the cache then the row already exists in the DB; update that
+          (contains? (cache/cache) setting-name) (update-setting! setting-name new-value)
+          ;; if there's nothing in the cache then the row doesn't exist, insert a new one
+          :else                                  (set-new-setting! setting-name new-value))
+        ;; update cached value
+        (cache/update-cache! setting-name new-value)
+        ;; Record the fact that a Setting has been updated so eventaully other instances (if applicable) find out about it
+        ;; (For Settings that don't use the Cache, don't update the `last-updated` value, because it will cause other
+        ;; instances to do needless reloading of the cache from the DB)
+        (when-not *disable-cache*
+          (cache/update-settings-last-updated!))
+        ;; Now return the `new-value`.
+        new-value))))
 
 (defn set-boolean!
   "Set the value of boolean `setting-or-name`. `new-value` can be nil, a boolean, or a string representation of one,
@@ -461,21 +344,22 @@
    This is used internally be `defsetting`; you shouldn't need to use it yourself."
   [{setting-name :name, setting-type :type, default :default, :as setting}]
   (u/prog1 (let [setting-type         (s/validate Type (or setting-type :string))]
-             (merge {:name        setting-name
-                     :description nil
-                     :type        setting-type
-                     :default     default
-                     :getter      (partial (default-getter-for-type setting-type) setting-name)
-                     :setter      (partial (default-setter-for-type setting-type) setting-name)
-                     :tag         (default-tag-for-type setting-type)
-                     :internal?   false
-                     :cache?      true}
+             (merge
+              {:name        setting-name
+               :description nil
+               :type        setting-type
+               :default     default
+               :getter      (partial (default-getter-for-type setting-type) setting-name)
+               :setter      (partial (default-setter-for-type setting-type) setting-name)
+               :tag         (default-tag-for-type setting-type)
+               :internal?   false
+               :sensitive?  false
+               :cache?      true}
                     (dissoc setting :name :type :default)))
     (s/validate SettingDefinition <>)
     (swap! registered-settings assoc setting-name <>)))
 
 
-
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                                defsetting macro                                                |
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -519,16 +403,15 @@
      ;; :refer-clojure :exclude doesn't seem to work in this case
      (metabase.models.setting/set! setting new-value))))
 
-(defn- expr-of-sym? [symbols expr]
-  (when-let [first-sym (and (coll? expr)
-                            (first expr))]
-    (some #(= first-sym %) symbols)))
+(defn- is-expression? [symbols expression]
+  (when (list? expression)
+    ((set symbols) (first expression))))
 
 (defn- valid-trs-or-tru? [desc]
-  (expr-of-sym? ['trs 'tru `trs `tru] desc))
+  (is-expression? #{'trs 'tru `trs `tru} desc))
 
 (defn- valid-str-of-trs-or-tru? [maybe-str-expr]
-  (when (expr-of-sym? ['str `str] maybe-str-expr)
+  (when (is-expression? #{'str `str} maybe-str-expr)
     ;; When there are several i18n'd sentences, there will probably be a surrounding `str` invocation and a space in
     ;; between the sentences, remove those to validate the i18n clauses
     (let [exprs-without-strs (remove (every-pred string? str/blank?) (rest maybe-str-expr))]
@@ -560,20 +443,25 @@
 
    You may optionally pass any of the OPTIONS below:
 
-   *  `:default`   - The default value of the setting. (default: `nil`)
-   *  `:type`      - `:string` (default), `:boolean`, `:integer`, or `:json`. Non-`:string` settings have special
-                     default getters and setters that automatically coerce values to the correct types.
-   *  `:internal?` - This Setting is for internal use and shouldn't be exposed in the UI (i.e., not returned by the
-                      corresponding endpoints). Default: `false`
-   *  `:getter`    - A custom getter fn, which takes no arguments. Overrides the default implementation. (This can in
-                     turn call functions in this namespace like `get-string` or `get-boolean` to invoke the default
-                     getter behavior.)
-   *  `:setter`    - A custom setter fn, which takes a single argument. Overrides the default implementation. (This
-                     can in turn call functions in this namespace like `set-string!` or `set-boolean!` to invoke the
-                     default setter behavior. Keep in mind that the custom setter may be passed `nil`, which should
-                     clear the values of the Setting.)
-   *  `:cache?`    - Should this Setting be cached? (default `true`)? Be careful when disabling this, because it could
-                     have a very negative performance impact."
+   *  `:default`    - The default value of the setting. (default: `nil`)
+   *  `:type`       - `:string` (default), `:boolean`, `:integer`, or `:json`. Non-`:string` settings have special
+                      default getters and setters that automatically coerce values to the correct types.
+   *  `:internal?`  - This Setting is for internal use and shouldn't be exposed in the UI (i.e., not returned by the
+                       corresponding endpoints). Default: `false`
+   *  `:getter`     - A custom getter fn, which takes no arguments. Overrides the default implementation. (This can in
+                      turn call functions in this namespace like `get-string` or `get-boolean` to invoke the default
+                      getter behavior.)
+   *  `:setter`     - A custom setter fn, which takes a single argument. Overrides the default implementation. (This
+                      can in turn call functions in this namespace like `set-string!` or `set-boolean!` to invoke the
+                      default setter behavior. Keep in mind that the custom setter may be passed `nil`, which should
+                      clear the values of the Setting.)
+   *  `:cache?`     - Should this Setting be cached? (default `true`)? Be careful when disabling this, because it could
+                      have a very negative performance impact.
+   *  `:sensitive?` - Is this a sensitive setting, such as a password, that we should never return in plaintext?
+                      (Default: `false`). Obfuscation is not done by getter functions, but instead by functions that
+                      ultimately return these values via the API, such as `all` below. (In other words, code in the
+                      backend can continute to consume sensitive Settings normally; sensitivity is a purely user-facing
+                      option.)"
   {:style/indent 1}
   [setting-symb description & {:as options}]
   {:pre [(symbol? setting-symb)]}
@@ -606,21 +494,46 @@
   ;; changed, and doesn't run when irrelevant changes (to other settings) are made.
   (events/publish-event! :settings-update settings))
 
+(defn- obfuscate-value
+  "Obfuscate the value of sensitive Setting. We'll still show the last 2 characters so admins can still check that the
+  value is what's expected (e.g. the correct password).
+
+    (obfuscate-value \"sensitivePASSWORD123\") ;; -> \"**********23\""
+  [s]
+  (str "**********" (str/join (take-last 2 (str s)))))
+
+(defn user-facing-value
+  "Get the value of a Setting that should be displayed to a User (i.e. via `/api/setting/` endpoints): for Settings set
+  via env vars, or Settings whose value has not been set (i.e., Settings whose value is the same as the default value)
+  no value is displayed; for sensitive Settings, the value is obfuscated."
+  [setting-or-name]
+  (let [{:keys [sensitive? default], k :name, :as setting} (resolve-setting setting-or-name)
+        v                                                  (get k)
+        value-is-default?                                  (= v default)
+        value-is-from-env-var?                             (= v (env-var-value setting))]
+    (cond
+      ;; TODO - Settings set via an env var aren't returned for security purposes. It is an open question whether we
+      ;; should obfuscate them and still show the last two characters like we do for sensitive values that are set via
+      ;; the UI.
+      (or value-is-default? value-is-from-env-var?)
+      nil
+
+      sensitive?
+      (obfuscate-value v)
+
+      :else
+      v)))
 
-(defn- user-facing-info [setting]
-  (let [k         (:name setting)
-        v         (get k)
-        env-value (env-var-value setting)]
+(defn- user-facing-info [{:keys [sensitive? default description], k :name, :as setting}]
+  (let [set-via-env-var? (boolean (env-var-value setting))]
     {:key            k
-     :value          (when (and (not= v env-value)
-                                (not= v (:default setting)))
-                       v)
-     :is_env_setting (boolean env-value)
+     :value          (user-facing-value setting)
+     :is_env_setting set-via-env-var?
      :env_name       (env-var-name setting)
-     :description    (str (:description setting))
-     :default        (or (when env-value
-                           (format "Using $%s" (env-var-name setting)))
-                         (:default setting))}))
+     :description    (str description)
+     :default        (if set-via-env-var?
+                       (tru "Using value of env var {0}" (str \$ (env-var-name setting)))
+                       default)}))
 
 (defn all
   "Return a sequence of Settings maps in a format suitable for consumption by the frontend.
diff --git a/src/metabase/models/setting/cache.clj b/src/metabase/models/setting/cache.clj
new file mode 100644
index 0000000000000000000000000000000000000000..07de4e1faa62cc5c1cd6b8f399f847c69fded9a4
--- /dev/null
+++ b/src/metabase/models/setting/cache.clj
@@ -0,0 +1,141 @@
+(ns metabase.models.setting.cache
+  "Settings cache. Cache is a 1:1 mapping of what's in the DB. Cached lookup time is ~60µs, compared to ~1800µs for DB
+  lookup."
+  (:require [clojure.core :as core]
+            [clojure.core.memoize :as memoize]
+            [clojure.java.jdbc :as jdbc]
+            [clojure.tools.logging :as log]
+            [honeysql.core :as hsql]
+            [metabase
+             [db :as mdb]
+             [util :as u]]
+            [metabase.util
+             [honeysql-extensions :as hx]
+             [i18n :as ui18n :refer [trs]]]
+            [toucan.db :as db]))
+
+(def ^:private cache*
+  "Settings cache. Map of Setting key (string) -> Setting value (string)."
+  (atom nil))
+
+(defn cache
+  "Fetch the current contents of the Settings cache, a map of key (string) -> value (string)."
+  []
+  @cache*)
+
+(defn update-cache!
+  "Update the String value of a Setting in the Settings cache."
+  [setting-name, ^String new-value]
+  (if (seq new-value)
+    (swap! cache* assoc  setting-name new-value)
+    (swap! cache* dissoc setting-name)))
+
+;; CACHE SYNCHRONIZATION
+;;
+;; When running multiple Metabase instances (horizontal scaling), it is of course possible for one instance to update
+;; a Setting, and, since Settings are cached (to avoid tons of DB calls), for the other instances to then have an
+;; out-of-date cache. Thus we need a way for instances to know when their caches are out of date, so they can update
+;; them accordingly. Here is our solution:
+;;
+;; We will record the last time *any* Setting was updated in a special Setting called `settings-last-updated`.
+;;
+;; Since `settings-last-updated` itself is a Setting, it will get fetched as part of each instance's local cache; we
+;; can then periodically compare the locally cached value of `settings-last-updated` with the value in the DB. If our
+;; locally cached value is older than the one in the DB, we will flush our cache. When the cache is fetched again, it
+;; will have the up-to-date value.
+;;
+;; Because different machines can have out-of-sync clocks, we'll rely entirely on the application DB for caclulating
+;; and comparing values of `settings-last-updated`. Because the Setting table itself only stores text values, we'll
+;; need to cast it between TEXT and TIMESTAMP SQL types as needed.
+
+(def ^String settings-last-updated-key "settings-last-updated")
+
+(defn update-settings-last-updated!
+  "Update the value of `settings-last-updated` in the DB; if the row does not exist, insert one."
+  []
+  (log/debug (trs "Updating value of settings-last-updated in DB..."))
+  ;; for MySQL, cast(current_timestamp AS char); for H2 & Postgres, cast(current_timestamp AS text)
+  (let [current-timestamp-as-string-honeysql (hx/cast (if (= (mdb/db-type) :mysql) :char :text)
+                                                      (hsql/raw "current_timestamp"))]
+    ;; attempt to UPDATE the existing row. If no row exists, `update-where!` will return false...
+    (or (db/update-where! 'Setting {:key settings-last-updated-key} :value current-timestamp-as-string-honeysql)
+        ;; ...at which point we will try to INSERT a new row. Note that it is entirely possible two instances can both
+        ;; try to INSERT it at the same time; one instance would fail because it would violate the PK constraint on
+        ;; `key`, and throw a SQLException. As long as one instance updates the value, we are fine, so we can go ahead
+        ;; and ignore that Exception if one is thrown.
+        (try
+          ;; Use `simple-insert!` because we do *not* want to trigger pre-insert behavior, such as encrypting `:value`
+          (db/simple-insert! 'Setting :key settings-last-updated-key, :value current-timestamp-as-string-honeysql)
+          (catch java.sql.SQLException e
+            ;; go ahead and log the Exception anyway on the off chance that it *wasn't* just a race condition issue
+            (log/error (trs "Error inserting a new Setting: {0}"
+                            (with-out-str (jdbc/print-sql-exception-chain e))))))))
+  ;; Now that we updated the value in the DB, go ahead and update our cached value as well, because we know about the
+  ;; changes
+  (swap! cache* assoc settings-last-updated-key (db/select-one-field :value 'Setting :key settings-last-updated-key)))
+
+(defn- cache-out-of-date?
+  "Check whether our Settings cache is out of date. We know the cache is out of date if either of the following
+  conditions is true:
+
+   *  The cache is empty (the `cache*` atom is `nil`), which of course means it needs to be updated
+   *  There is a value of `settings-last-updated` in the cache, and it is older than the value of in the DB. (There
+      will be no value until the first time a normal Setting is updated; thus if it is not yet set, we do not yet need
+      to invalidate our cache.)"
+  []
+  (log/debug (trs "Checking whether settings cache is out of date (requires DB call)..."))
+  (boolean
+   (or
+    ;; is the cache empty?
+    (not @cache*)
+    ;; if not, get the cached value of `settings-last-updated`, and if it exists...
+    (when-let [last-known-update (core/get @cache* settings-last-updated-key)]
+      ;; compare it to the value in the DB. This is done be seeing whether a row exists
+      ;; WHERE value > <local-value>
+      (u/prog1 (db/select-one 'Setting
+                 {:where [:and
+                          [:= :key settings-last-updated-key]
+                          [:> :value last-known-update]]})
+        (when <>
+          (log/info (u/format-color 'red
+                        (str (trs "Settings have been changed on another instance, and will be reloaded here."))))))))))
+
+(def ^:private cache-update-check-interval-ms
+  "How often we should check whether the Settings cache is out of date (which requires a DB call)?"
+  ;; once a minute
+  (* 60 1000))
+
+(def ^:private ^{:arglists '([])} should-restore-cache?
+  "TTL-memoized version of `cache-out-of-date?`. Call this function to see whether we need to repopulate the cache with
+  values from the DB."
+  (memoize/ttl cache-out-of-date? :ttl/threshold cache-update-check-interval-ms))
+
+(def ^:private restore-cache-if-needed-lock (Object.))
+
+(defn restore-cache-if-needed!
+  "Check whether we need to repopulate the cache with fresh values from the DB (because the cache is either empty or
+  known to be out-of-date), and do so if needed. This is intended to be called every time a Setting value is
+  retrieved, so it should be efficient; thus the calculation (`should-restore-cache?`) is itself TTL-memoized."
+  []
+  ;; There's a potential race condition here where two threads both call this at the exact same moment, and both get
+  ;; `true` when they call `should-restore-cache`, and then both simultaneously try to update the cache (or, one
+  ;; updates the cache, but the other calls `should-restore-cache?` and gets `true` before the other calls
+  ;; `memo-swap!` (see below))
+  ;;
+  ;; This is not desirable, since either situation would result in duplicate work. Better to just add a quick lock
+  ;; here so only one of them does it, since at any rate waiting for the other thread to finish the task in progress is
+  ;; certainly quicker than starting the task ourselves from scratch
+  (locking restore-cache-if-needed-lock
+    (when (should-restore-cache?)
+      (log/debug (trs "Refreshing Settings cache..."))
+      (reset! cache* (db/select-field->field :key :value 'Setting))
+      ;; Now the cache is up-to-date. That is all good, but if we call `should-restore-cache?` again in a second it
+      ;; will still return `true`, because its result is memoized, and we would be on the hook to (again) update the
+      ;; cache. So go ahead and clear the memozied results for `should-restore-cache?`. The next time around when
+      ;; someone calls this it will cache the latest value (which should be `false`)
+      ;;
+      ;; NOTE: I tried using `memo-swap!` instead to set the cached response to `false` here, avoiding the extra DB
+      ;; call the next fn call would make, but it didn't seem to work correctly (I think it was still discarding the
+      ;; new value because of the TTL). So we will just stick with `memo-clear!` for now. (One extra DB call whenever
+      ;; the cache gets invalidated shouldn't be a huge deal)
+      (memoize/memo-clear! should-restore-cache?))))
diff --git a/test/metabase/api/setting_test.clj b/test/metabase/api/setting_test.clj
index 1608d91bf8026ebf17e170ff925674834bdf9687..8dfadd6e4e9588aa1264e876b8f2aed2663c0019 100644
--- a/test/metabase/api/setting_test.clj
+++ b/test/metabase/api/setting_test.clj
@@ -1,7 +1,7 @@
 (ns metabase.api.setting-test
-  (:require [expectations :refer :all]
-            [metabase.models.setting-test :refer [set-settings! test-setting-1 test-setting-2]]
-            [metabase.test.data.users :refer :all]))
+  (:require [expectations :refer [expect]]
+            [metabase.models.setting-test :refer [set-settings! test-sensitive-setting test-setting-1 test-setting-2]]
+            [metabase.test.data.users :refer [user->client]]))
 
 ;; ## Helper Fns
 (defn- fetch-test-settings  []
@@ -15,8 +15,18 @@
 ;; ## GET /api/setting
 ;; Check that we can fetch all Settings for Org
 (expect
- [{:key "test-setting-1", :value nil,     :is_env_setting true,  :env_name "MB_TEST_SETTING_1", :description "Test setting - this only shows up in dev (1)", :default "Using $MB_TEST_SETTING_1"}
-  {:key "test-setting-2", :value "FANCY", :is_env_setting false, :env_name "MB_TEST_SETTING_2", :description "Test setting - this only shows up in dev (2)", :default "[Default Value]"}]
+  [{:key            "test-setting-1"
+    :value          nil
+    :is_env_setting true
+    :env_name       "MB_TEST_SETTING_1"
+    :description    "Test setting - this only shows up in dev (1)"
+    :default        "Using value of env var $MB_TEST_SETTING_1"}
+   {:key            "test-setting-2"
+    :value          "FANCY"
+    :is_env_setting false
+    :env_name       "MB_TEST_SETTING_2"
+    :description    "Test setting - this only shows up in dev (2)"
+    :default        "[Default Value]"}]
  (do (set-settings! nil "FANCY")
      (fetch-test-settings)))
 
@@ -44,3 +54,52 @@
 ;; ## Check non-superuser can't set a Setting
 (expect "You don't have permissions to do that."
   ((user->client :rasta) :put 403 "setting/test-setting-1" {:value "NICE!"}))
+
+
+;;; ----------------------------------------------- Sensitive Settings -----------------------------------------------
+
+;; Sensitive settings should always come back obfuscated
+
+;; GET /api/setting/:name should obfuscate sensitive settings
+(expect
+  "**********EF"
+  (do
+    (test-sensitive-setting "ABCDEF")
+    (fetch-setting :test-sensitive-setting)))
+
+;; GET /api/setting should obfuscate sensitive settings
+(expect
+  {:key            "test-sensitive-setting"
+   :value          "**********LM"
+   :is_env_setting false
+   :env_name       "MB_TEST_SENSITIVE_SETTING"
+   :description    "This is a sample sensitive Setting."
+   :default        nil}
+  (do
+    (test-sensitive-setting "GHIJKLM")
+    (some (fn [{setting-name :key, :as setting}]
+            (when (= setting-name "test-sensitive-setting")
+              setting))
+          ((user->client :crowberto) :get 200 "setting"))))
+
+;; Setting the Setting via an endpoint should still work as expected; the normal getter functions should *not*
+;; obfuscate sensitive Setting values -- that should be done by the API
+(expect
+  "123456"
+  (do
+    ((user->client :crowberto) :put 200 "setting/test-sensitive-setting" {:value "123456"})
+    (test-sensitive-setting)))
+
+;; Attempts to set the Setting to an obfuscated value should be ignored
+(expect
+  "123456"
+  (do
+    (test-sensitive-setting "123456")
+    ((user->client :crowberto) :put 200 "setting/test-sensitive-setting" {:value "**********56"})
+    (test-sensitive-setting)))
+
+(expect
+  (do
+    (test-sensitive-setting "123456")
+    ((user->client :crowberto) :put 200 "setting" {:test-sensitive-setting "**********56"})
+    (test-sensitive-setting)))
diff --git a/test/metabase/models/setting/cache_test.clj b/test/metabase/models/setting/cache_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..1cc922cd229fc47255d985ebe037ef1b75e2d32d
--- /dev/null
+++ b/test/metabase/models/setting/cache_test.clj
@@ -0,0 +1,124 @@
+(ns metabase.models.setting.cache-test
+  (:require [clojure.core.memoize :as memoize]
+            [expectations :refer [expect]]
+            [honeysql.core :as hsql]
+            [metabase.db :as mdb]
+            [metabase.models
+             [setting :refer [Setting]]
+             [setting-test :as setting-test]]
+            [metabase.models.setting.cache :as cache]
+            [toucan.db :as db]))
+
+;;; --------------------------------------------- Cache Synchronization ----------------------------------------------
+
+(defn- clear-cache! []
+  (reset! @#'metabase.models.setting.cache/cache* nil))
+
+(defn- settings-last-updated-value-in-cache []
+  (get (cache/cache) cache/settings-last-updated-key))
+
+(defn- update-settings-last-updated-value-in-db!
+  "Simulate a different instance updating the value of `settings-last-updated` in the DB by updating its value without
+  updating our locally cached value.."
+  []
+  (db/update-where! Setting {:key cache/settings-last-updated-key}
+    :value (hsql/raw (case (mdb/db-type)
+                       ;; make it one second in the future so we don't end up getting an exact match when we try to test
+                       ;; to see if things update below
+                       :h2       "cast(dateadd('second', 1, current_timestamp) AS text)"
+                       :mysql    "cast((current_timestamp + interval 1 second) AS char)"
+                       :postgres "cast((current_timestamp + interval '1 second') AS text)"))))
+
+(defn- simulate-another-instance-updating-setting! [setting-name new-value]
+  (db/update-where! Setting {:key (name setting-name)} :value new-value)
+  (update-settings-last-updated-value-in-db!))
+
+(defn- flush-memoized-results-for-should-restore-cache!
+  "Remove any memoized results for `should-restore-cache?`, so we can test `restore-cache-if-needed!` works the way we'd
+  expect."
+  []
+  (memoize/memo-clear! @#'cache/should-restore-cache?))
+
+;; When I update a Setting, does it set/update `settings-last-updated`?
+(expect
+  (do
+    (setting-test/clear-settings-last-updated-value-in-db!)
+    (setting-test/toucan-name "Bird Can")
+    (string? (setting-test/settings-last-updated-value-in-db))))
+
+;; ...and is the value updated in the cache as well?
+(expect
+  (do
+    (clear-cache!)
+    (setting-test/toucan-name "Bird Can")
+    (string? (settings-last-updated-value-in-cache))))
+
+;; ...and if I update it again, will the value be updated?
+(expect
+  (do
+    (setting-test/clear-settings-last-updated-value-in-db!)
+    (setting-test/toucan-name "Bird Can")
+    (let [first-value (setting-test/settings-last-updated-value-in-db)]
+      ;; MySQL only has the resolution of one second on the timestamps here so we should wait that long to make sure
+      ;; the second-value actually ends up being greater than the first
+      (Thread/sleep 1200)
+      (setting-test/toucan-name "Bird Can")
+      (let [second-value (setting-test/settings-last-updated-value-in-db)]
+        ;; first & second values should be different, and first value should be "less than" the second value
+        (and (not= first-value second-value)
+             (neg? (compare first-value second-value)))))))
+
+;; If there is no cache, it should be considered out of date!`
+(expect
+  (do
+    (clear-cache!)
+    (#'cache/cache-out-of-date?)))
+
+;; But if I set a setting, it should cause the cache to be populated, and be up-to-date
+(expect
+  false
+  (do
+    (clear-cache!)
+    (setting-test/toucan-name "Reggae Toucan")
+    (#'cache/cache-out-of-date?)))
+
+;; If another instance updates a Setting, `cache-out-of-date?` should return `true` based on DB comparisons...
+;; be true!
+(expect
+  (do
+    (clear-cache!)
+    (setting-test/toucan-name "Reggae Toucan")
+    (simulate-another-instance-updating-setting! :setting-test/toucan-name "Bird Can")
+    (#'cache/cache-out-of-date?)))
+
+;; of course, `restore-cache-if-needed!` should use TTL memoization, and the cache should not get updated right away
+;; even if another instance updates a value...
+(expect
+  "Sam"
+  (do
+    (flush-memoized-results-for-should-restore-cache!)
+    (clear-cache!)
+    (setting-test/toucan-name "Sam")                 ; should restore cache, and put in {"setting-test/toucan-name" "Sam"}
+    ;; since we cleared the memoized value of `should-restore-cache?` call it again to make sure it gets set to
+    ;; `false` as it would IRL if we were calling it again from the same instance
+    (#'cache/should-restore-cache?)
+    ;; now have another instance change the value
+    (simulate-another-instance-updating-setting! :setting-test/toucan-name "Bird Can")
+    ;; our cache should not be updated yet because it's on a TTL
+    (setting-test/toucan-name)))
+
+;; ...and when it comes time to check our cache for updating (when calling `restore-cache-if-needed!`, it should get
+;; the updated value. (we're not actually going to wait a minute for the memoized values of `should-restore-cache?` to
+;; be invalidated, so we will manually flush the memoization cache to simulate it happening)
+(expect
+  "Bird Can"
+  (do
+    (clear-cache!)
+    (setting-test/toucan-name "Reggae Toucan")
+    (simulate-another-instance-updating-setting! :setting-test/toucan-name "Bird Can")
+    (flush-memoized-results-for-should-restore-cache!)
+    ;; calling `setting-test/toucan-name` will call `restore-cache-if-needed!`, which will in turn call `should-restore-cache?`.
+    ;; Since memoized value is no longer present, this should call `cache-out-of-date?`, which checks the DB; it will
+    ;; detect a cache out-of-date situation and flush the cache as appropriate, giving us the updated value when we
+    ;; call! :wow:
+    (setting-test/toucan-name)))
diff --git a/test/metabase/models/setting_test.clj b/test/metabase/models/setting_test.clj
index 006752d48d590058e6051c4f5769ea8f13f4dc34..db4548dde099ff633beb1d8d9c0b9e3f0e913353 100644
--- a/test/metabase/models/setting_test.clj
+++ b/test/metabase/models/setting_test.clj
@@ -1,12 +1,9 @@
 (ns metabase.models.setting-test
-  (:require [clojure.core.memoize :as memoize]
-            [expectations :refer :all]
-            [honeysql.core :as hsql]
-            [metabase
-             [db :as mdb]
-             [util :as u]]
+  (:require [expectations :refer [expect]]
             [metabase.models.setting :as setting :refer [defsetting Setting]]
+            [metabase.models.setting.cache :as cache]
             [metabase.test.util :refer :all]
+            [metabase.util :as u]
             [metabase.util
              [encryption :as encryption]
              [encryption-test :as encryption-test]
@@ -151,12 +148,12 @@
 
 ;; #'setting/user-facing-info w/ no db value, env var value, no default value -- shouldn't leak env var value
 (expect
-  {:value nil, :is_env_setting true, :env_name "MB_TEST_SETTING_1", :default "Using $MB_TEST_SETTING_1"}
+  {:value nil, :is_env_setting true, :env_name "MB_TEST_SETTING_1", :default "Using value of env var $MB_TEST_SETTING_1"}
   (user-facing-info-with-db-and-env-var-values :test-setting-1 nil "TOUCANS"))
 
 ;; #'setting/user-facing-info w/ no db value, env var value, default value
 (expect
-  {:value nil,  :is_env_setting true, :env_name "MB_TEST_SETTING_2", :default "Using $MB_TEST_SETTING_2"}
+  {:value nil,  :is_env_setting true, :env_name "MB_TEST_SETTING_2", :default "Using value of env var $MB_TEST_SETTING_2"}
   (user-facing-info-with-db-and-env-var-values :test-setting-2 nil "TOUCANS"))
 
 ;; #'setting/user-facing-info w/ db value, no env var value, no default value
@@ -172,13 +169,13 @@
 ;; #'setting/user-facing-info w/ db value, env var value, no default value -- the DB value should take precedence over
 ;; #the env var
 (expect
-  {:value "WOW", :is_env_setting true, :env_name "MB_TEST_SETTING_1", :default "Using $MB_TEST_SETTING_1"}
+  {:value "WOW", :is_env_setting true, :env_name "MB_TEST_SETTING_1", :default "Using value of env var $MB_TEST_SETTING_1"}
   (user-facing-info-with-db-and-env-var-values :test-setting-1 "WOW" "ENV VAR"))
 
 ;; #'setting/user-facing-info w/ db value, env var value, default value -- env var should take precedence over default
 ;; #value
 (expect
-  {:value "WOW", :is_env_setting true, :env_name "MB_TEST_SETTING_2", :default "Using $MB_TEST_SETTING_2"}
+  {:value "WOW", :is_env_setting true, :env_name "MB_TEST_SETTING_2", :default "Using value of env var $MB_TEST_SETTING_2"}
   (user-facing-info-with-db-and-env-var-values :test-setting-2 "WOW" "ENV VAR"))
 
 ;; all
@@ -202,7 +199,7 @@
     :is_env_setting true
     :env_name       "MB_TEST_SETTING_1"
     :description    "Test setting - this only shows up in dev (1)"
-    :default        "Using $MB_TEST_SETTING_1"}
+    :default        "Using value of env var $MB_TEST_SETTING_1"}
    {:key            :test-setting-2
     :value          "S2"
     :is_env_setting false,
@@ -240,7 +237,7 @@
 
 ;; boolean settings shouldn't be obfuscated when set by env var
 (expect
-  {:value true, :is_env_setting true, :env_name "MB_TEST_BOOLEAN_SETTING", :default "Using $MB_TEST_BOOLEAN_SETTING"}
+  {:value true, :is_env_setting true, :env_name "MB_TEST_BOOLEAN_SETTING", :default "Using value of env var $MB_TEST_BOOLEAN_SETTING"}
   (user-facing-info-with-db-and-env-var-values :test-boolean-setting nil "true"))
 
 ;; env var values should be case-insensitive
@@ -291,7 +288,7 @@
 ;; make sure that if for some reason the cache gets out of sync it will reset so we can still set new settings values
 ;; (#4178)
 
-(setting/defsetting ^:private toucan-name
+(setting/defsetting toucan-name
   "Name for the Metabase Toucan mascot."
   :internal? true)
 
@@ -301,7 +298,7 @@
     ;; clear out any existing values of `toucan-name`
     (db/simple-delete! setting/Setting {:key "toucan-name"})
     ;; restore the cache
-    ((resolve 'metabase.models.setting/restore-cache-if-needed!))
+    (cache/restore-cache-if-needed!)
     ;; now set a value for the `toucan-name` setting the wrong way
     (db/insert! setting/Setting {:key "toucan-name", :value "Reggae"})
     ;; ok, now try to set the Setting the correct way
@@ -361,130 +358,13 @@
       (test-timestamp-setting)))
 
 
-;;; --------------------------------------------- Cache Synchronization ----------------------------------------------
-
-(def ^:private settings-last-updated-key @(resolve 'metabase.models.setting/settings-last-updated-key))
-
-(defn- clear-settings-last-updated-value-in-db! []
-  (db/simple-delete! Setting {:key settings-last-updated-key}))
-
-(defn- settings-last-updated-value-in-db []
-  (db/select-one-field :value Setting :key settings-last-updated-key))
-
-(defn- clear-cache! []
-  (reset! @(resolve 'metabase.models.setting/cache) nil))
-
-(defn- settings-last-updated-value-in-cache []
-  (get @@(resolve 'metabase.models.setting/cache) settings-last-updated-key))
-
-(defn- update-settings-last-updated-value-in-db!
-  "Simulate a different instance updating the value of `settings-last-updated` in the DB by updating its value without
-  updating our locally cached value.."
-  []
-  (db/update-where! Setting {:key settings-last-updated-key}
-    :value (hsql/raw (case (mdb/db-type)
-                       ;; make it one second in the future so we don't end up getting an exact match when we try to test
-                       ;; to see if things update below
-                       :h2       "cast(dateadd('second', 1, current_timestamp) AS text)"
-                       :mysql    "cast((current_timestamp + interval 1 second) AS char)"
-                       :postgres "cast((current_timestamp + interval '1 second') AS text)"))))
-
-(defn- simulate-another-instance-updating-setting! [setting-name new-value]
-  (db/update-where! Setting {:key (name setting-name)} :value new-value)
-  (update-settings-last-updated-value-in-db!))
-
-(defn- flush-memoized-results-for-should-restore-cache!
-  "Remove any memoized results for `should-restore-cache?`, so we can test `restore-cache-if-needed!` works the way we'd
-  expect."
-  []
-  (memoize/memo-clear! @(resolve 'metabase.models.setting/should-restore-cache?)))
-
-;; When I update a Setting, does it set/update `settings-last-updated`?
-(expect
-  (do
-    (clear-settings-last-updated-value-in-db!)
-    (toucan-name "Bird Can")
-    (string? (settings-last-updated-value-in-db))))
-
-;; ...and is the value updated in the cache as well?
-(expect
-  (do
-    (clear-cache!)
-    (toucan-name "Bird Can")
-    (string? (settings-last-updated-value-in-cache))))
-
-;; ...and if I update it again, will the value be updated?
-(expect
-  (do
-    (clear-settings-last-updated-value-in-db!)
-    (toucan-name "Bird Can")
-    (let [first-value (settings-last-updated-value-in-db)]
-      ;; MySQL only has the resolution of one second on the timestamps here so we should wait that long to make sure
-      ;; the second-value actually ends up being greater than the first
-      (Thread/sleep 1200)
-      (toucan-name "Bird Can")
-      (let [second-value (settings-last-updated-value-in-db)]
-        ;; first & second values should be different, and first value should be "less than" the second value
-        (and (not= first-value second-value)
-             (neg? (compare first-value second-value)))))))
-
-;; If there is no cache, it should be considered out of date!`
-(expect
-  (do
-    (clear-cache!)
-    (#'setting/cache-out-of-date?)))
-
-;; But if I set a setting, it should cause the cache to be populated, and be up-to-date
-(expect
-  false
-  (do
-    (clear-cache!)
-    (toucan-name "Reggae Toucan")
-    (#'setting/cache-out-of-date?)))
-
-;; If another instance updates a Setting, `cache-out-of-date?` should return `true` based on DB comparisons...
-;; be true!
-(expect
-  (do
-    (clear-cache!)
-    (toucan-name "Reggae Toucan")
-    (simulate-another-instance-updating-setting! :toucan-name "Bird Can")
-    (#'setting/cache-out-of-date?)))
-
-;; of course, `restore-cache-if-needed!` should use TTL memoization, and the cache should not get updated right away
-;; even if another instance updates a value...
-(expect
-  "Sam"
-  (do
-    (flush-memoized-results-for-should-restore-cache!)
-    (clear-cache!)
-    (toucan-name "Sam")                 ; should restore cache, and put in {"toucan-name" "Sam"}
-    ;; since we cleared the memoized value of `should-restore-cache?` call it again to make sure it gets set to
-    ;; `false` as it would IRL if we were calling it again from the same instance
-    (#'setting/should-restore-cache?)
-    ;; now have another instance change the value
-    (simulate-another-instance-updating-setting! :toucan-name "Bird Can")
-    ;; our cache should not be updated yet because it's on a TTL
-    (toucan-name)))
-
-;; ...and when it comes time to check our cache for updating (when calling `restore-cache-if-needed!`, it should get
-;; the updated value. (we're not actually going to wait a minute for the memoized values of `should-restore-cache?` to
-;; be invalidated, so we will manually flush the memoization cache to simulate it happening)
-(expect
-  "Bird Can"
-  (do
-    (clear-cache!)
-    (toucan-name "Reggae Toucan")
-    (simulate-another-instance-updating-setting! :toucan-name "Bird Can")
-    (flush-memoized-results-for-should-restore-cache!)
-    ;; calling `toucan-name` will call `restore-cache-if-needed!`, which will in turn call `should-restore-cache?`.
-    ;; Since memoized value is no longer present, this should call `cache-out-of-date?`, which checks the DB; it will
-    ;; detect a cache out-of-date situation and flush the cache as appropriate, giving us the updated value when we
-    ;; call! :wow:
-    (toucan-name)))
+;;; ----------------------------------------------- Uncached Settings ------------------------------------------------
 
+(defn clear-settings-last-updated-value-in-db! []
+  (db/simple-delete! Setting {:key cache/settings-last-updated-key}))
 
-;;; ----------------------------------------------- Uncached Settings ------------------------------------------------
+(defn settings-last-updated-value-in-db []
+  (db/select-one-field :value Setting :key cache/settings-last-updated-key))
 
 (defsetting ^:private uncached-setting
   "A test setting that should *not* be cached."
@@ -514,3 +394,25 @@
     (clear-settings-last-updated-value-in-db!)
     (uncached-setting "abcdef")
     (settings-last-updated-value-in-db)))
+
+
+;;; ----------------------------------------------- Sensitive Settings -----------------------------------------------
+
+(defsetting test-sensitive-setting
+  (tru "This is a sample sensitive Setting.")
+  :sensitive? true)
+
+;; `user-facing-value` should obfuscate sensitive settings
+(expect
+  "**********23"
+  (do
+    (test-sensitive-setting "ABC123")
+    (setting/user-facing-value "test-sensitive-setting")))
+
+;; Attempting to set a sensitive setting to an obfuscated value should be ignored -- it was probably done accidentally
+(expect
+  "123456"
+  (do
+    (test-sensitive-setting "123456")
+    (test-sensitive-setting "**********56")
+    (test-sensitive-setting)))