From 94756f199453f2ea3f8e7c147540e17837ba2543 Mon Sep 17 00:00:00 2001 From: Daniel Woelfel Date: Fri, 7 Aug 2026 20:08:39 -0700 Subject: [PATCH] override most config values with env var --- server/src/instant/config_edn.clj | 82 ++++++++++++++++++++----- server/test/instant/config_edn_test.clj | 16 +++++ 2 files changed, 83 insertions(+), 15 deletions(-) diff --git a/server/src/instant/config_edn.clj b/server/src/instant/config_edn.clj index bdb569ee39..100e0e8546 100644 --- a/server/src/instant/config_edn.clj +++ b/server/src/instant/config_edn.clj @@ -2,6 +2,7 @@ (:require [clojure.edn :as edn] [clojure.java.io :as io] [clojure.spec.alpha :as s] + [clojure.string] [clojure.walk :as w] [clojure.tools.logging :as log]) (:import (org.apache.commons.codec.binary Hex))) @@ -148,6 +149,53 @@ (def associated-data (.getBytes "config")) +(defn- keys-spec-keys + "Returns the unqualified config keys declared by an `s/keys` spec." + [spec-kw] + (let [opts (apply hash-map (rest (s/form spec-kw)))] + (map (comp keyword name) + (concat (:req-un opts) (:opt-un opts))))) + +(def config-keys + "Every config key known to the spec, whether it's required in dev or prod." + (delay (distinct (concat (keys-spec-keys ::config) + (keys-spec-keys ::config-prod))))) + +(defn- overridable-key-type + "Classifies a config key as :secret (a `::config-value`, stored obfuscated), + :plain (a bare string), or nil for structured keys we can't represent as a + single env var (keysets, oauth clients, etc.)." + [config-key] + (let [spec-kw (keyword (namespace ::config) (name config-key))] + (when (s/get-spec spec-kw) + (let [form (s/form spec-kw)] + (cond + (= form 'clojure.core/string?) :plain + (= form (s/form ::config-value)) :secret))))) + +(defn config-env-var-name + "The env var that overrides `config-key`, e.g. :s3-endpoint -> + INSTANT_CONFIG_S3_ENDPOINT." + [config-key] + (str "INSTANT_CONFIG_" + (-> (name config-key) + (clojure.string/upper-case) + (clojure.string/replace "-" "_")))) + +(defn env-overrides + "Reads INSTANT_CONFIG_* env vars for every string-valued config key, so any + such setting in the edn file can be overridden from the environment. Secrets + are obfuscated so they read back like the decrypted config." + [obfuscate] + (reduce (fn [acc k] + (if-let [t (overridable-key-type k)] + (if-let [v (System/getenv (config-env-var-name k))] + (assoc acc k (if (= t :secret) (obfuscate v) v)) + acc) + acc)) + {} + @config-keys)) + (defn decrypted-config "Given a config edn, decrypts the config and obfsucates the secrets Takes `obfuscate`, `get-hybrid-decrypt-primitive`, and `hybrid-decrypt` @@ -165,18 +213,22 @@ (hybrid-decrypt hybrid {:ciphertext (Hex/decodeHex ^String hex-string) :associated-data associated-data}))] - (w/postwalk - (fn [x] - (if-not (and (vector? x) - (keyword? (first x)) - (= (namespace (first x)) - (namespace ::test))) - x - (case (first x) - ::plain (obfuscate (second x)) - ::encoded (-> ^bytes (decrypt (-> x second :enc)) - (String.) - obfuscate) - ::encoded-bytes (-> ^bytes (decrypt (-> x second :enc-bytes)) - obfuscate)))) - (s/conform ::config config-edn)))) + (merge + (w/postwalk + (fn [x] + (if-not (and (vector? x) + (keyword? (first x)) + (= (namespace (first x)) + (namespace ::test))) + x + (case (first x) + ::plain (obfuscate (second x)) + ::encoded (-> ^bytes (decrypt (-> x second :enc)) + (String.) + obfuscate) + ::encoded-bytes (-> ^bytes (decrypt (-> x second :enc-bytes)) + obfuscate)))) + (s/conform ::config config-edn)) + ;; Any string-valued config key can be overridden from the environment + ;; via INSTANT_CONFIG_. + (env-overrides obfuscate)))) diff --git a/server/test/instant/config_edn_test.clj b/server/test/instant/config_edn_test.clj index 4893703048..17cc51630b 100644 --- a/server/test/instant/config_edn_test.clj +++ b/server/test/instant/config_edn_test.clj @@ -41,6 +41,22 @@ x) config))) +(deftest env-override-naming + (is (= "INSTANT_CONFIG_S3_ENDPOINT" + (config-edn/config-env-var-name :s3-endpoint))) + (is (= "INSTANT_CONFIG_POSTMARK_TOKEN" + (config-edn/config-env-var-name :postmark-token)))) + +(deftest env-overrides-obfuscate-secrets-only + ;; With no INSTANT_CONFIG_* env vars set, there's nothing to override. + (is (= {} (config-edn/env-overrides crypt-util/obfuscate))) + ;; A string-valued config key is overridable; a structured one is not. + (let [overridable? #(some? (#'config-edn/overridable-key-type %))] + (is (overridable? :s3-endpoint)) + (is (overridable? :postmark-token)) + (is (not (overridable? :google-oauth-client))) + (is (not (overridable? :aead-keyset))))) + (deftest config-smoketest (testing "dev config" ;; If this test fails, then there is either something wrong