diff --git a/include/libcamera/internal/global_configuration.h b/include/libcamera/internal/global_configuration.h
index 5eb646e33fc2..0d2ccb8a1808 100644
--- a/include/libcamera/internal/global_configuration.h
+++ b/include/libcamera/internal/global_configuration.h
@@ -42,13 +42,6 @@ public:
 
 	std::optional<std::vector<std::string>> listOption(
 		const std::initializer_list<std::string_view> confPath) const;
-	std::optional<std::string> envOption(
-		const char *const envVariable,
-		const std::initializer_list<std::string_view> confPath) const;
-	std::optional<std::vector<std::string>> envListOption(
-		const char *const envVariable,
-		const std::initializer_list<std::string_view> confPath,
-		const std::string delimiter = ":") const;
 
 private:
 	void load();
diff --git a/src/libcamera/camera_manager.cpp b/src/libcamera/camera_manager.cpp
index f774bd84291b..0dd4e0c590a1 100644
--- a/src/libcamera/camera_manager.cpp
+++ b/src/libcamera/camera_manager.cpp
@@ -114,9 +114,7 @@ void CameraManager::Private::createPipelineHandlers()
 	 * there is no configuration file.
 	 */
 	const auto pipesList =
-		configuration().envListOption("LIBCAMERA_PIPELINES_MATCH_LIST",
-					      { "pipelines_match_list" },
-					      ",");
+		configuration().listOption({ "pipelines_match_list" });
 	if (pipesList.has_value()) {
 		/*
 		 * When a list of preferred pipelines is defined, iterate
diff --git a/src/libcamera/global_configuration.cpp b/src/libcamera/global_configuration.cpp
index c87f56c2d822..75c07949e1ad 100644
--- a/src/libcamera/global_configuration.cpp
+++ b/src/libcamera/global_configuration.cpp
@@ -7,6 +7,7 @@
 
 #include "libcamera/internal/global_configuration.h"
 
+#include <array>
 #include <filesystem>
 #include <memory>
 #include <optional>
@@ -30,6 +31,99 @@ const std::vector<std::filesystem::path> globalConfigurationFiles = {
 	std::filesystem::path(LIBCAMERA_DATA_DIR) / "configuration.yaml",
 };
 
+class EnvironmentProcessor
+{
+public:
+	virtual ~EnvironmentProcessor() = default;
+
+	virtual void process(ValueNode &node, const char *env) = 0;
+};
+
+/* A processor that sets a fixed value. */
+template<typename T>
+class EnvironmentFixedProcessor : public EnvironmentProcessor
+{
+public:
+	EnvironmentFixedProcessor(const T &value)
+		: value_(value)
+	{
+	}
+
+	void process(ValueNode &node, [[maybe_unused]] const char *env) override
+	{
+		node.set(value_);
+	}
+
+private:
+	T value_;
+};
+
+/*
+ * A processor that parses the environment variable as a list of strings with a
+ * custom delimiter.
+ */
+class EnvironmentListProcessor : public EnvironmentProcessor
+{
+public:
+	EnvironmentListProcessor(const char *delimiter)
+		: delimiter_(delimiter)
+	{
+	}
+
+	void process(ValueNode &node, const char *env) override
+	{
+		for (auto &&value : utils::split(env, delimiter_))
+			node.add(std::make_unique<ValueNode>(std::move(value)));
+	}
+
+private:
+	const std::string delimiter_;
+};
+
+/* A processor that copies the value of the environment variable. */
+class EnvironmentValueProcessor : public EnvironmentProcessor
+{
+public:
+	void process(ValueNode &node, const char *env) override
+	{
+		node.set(std::string{ env });
+	}
+};
+
+struct EnvironmentOverride {
+	const char *variable;
+	std::initializer_list<std::string_view> path;
+	std::unique_ptr<EnvironmentProcessor> processor;
+};
+
+const std::array<EnvironmentOverride, 6> environmentOverrides{ {
+	{
+		"LIBCAMERA_IPA_CONFIG_PATH",
+		{ "ipa", "config_paths" },
+		std::make_unique<EnvironmentListProcessor>(":"),
+	}, {
+		"LIBCAMERA_IPA_FORCE_ISOLATION",
+		{ "ipa", "force_isolation" },
+		std::make_unique<EnvironmentFixedProcessor<bool>>(true),
+	}, {
+		"LIBCAMERA_IPA_MODULE_PATH",
+		{ "ipa", "module_paths" },
+		std::make_unique<EnvironmentListProcessor>(":"),
+	}, {
+		"LIBCAMERA_IPA_PROXY_PATH",
+		{ "ipa", "proxy_paths" },
+		std::make_unique<EnvironmentListProcessor>(":"),
+	}, {
+		"LIBCAMERA_PIPELINES_MATCH_LIST",
+		{ "pipelines_match_list" },
+		std::make_unique<EnvironmentListProcessor>(","),
+	}, {
+		"LIBCAMERA_SOFTISP_MODE",
+		{ "software_isp", "mode" },
+		std::make_unique<EnvironmentValueProcessor>(),
+	},
+} };
+
 } /* namespace */
 
 LOG_DEFINE_CATEGORY(Configuration)
@@ -50,9 +144,9 @@ LOG_DEFINE_CATEGORY(Configuration)
  * reported and no configuration file is used. This is to prevent libcamera from
  * using an unintended configuration file.
  *
- * The configuration can be accessed using the provided helpers, namely
- * option(), envOption(), listOption() and envListOption() to access individual
- * options, or configuration() to access the whole configuration.
+ * The configuration can be accessed using the provided helpers, namely option()
+ * and listOption() to access individual options, or configuration() to access
+ * the whole configuration.
  */
 
 /**
@@ -66,6 +160,25 @@ GlobalConfiguration::GlobalConfiguration()
 		configuration_->add("version", std::make_unique<ValueNode>(1));
 		configuration_->add("configuration", std::make_unique<ValueNode>());
 	}
+
+	/* Process environment variables that override configuration options. */
+	ValueNode *cfg = configuration_->at("configuration");
+
+	for (const EnvironmentOverride &envOverride : environmentOverrides) {
+		const char *envValue = utils::secure_getenv(envOverride.variable);
+		if (!envValue || !envValue[0])
+			continue;
+
+		std::unique_ptr<ValueNode> node = std::make_unique<ValueNode>();
+		envOverride.processor->process(*node.get(), envValue);
+
+		cfg->erase(envOverride.path);
+
+		if (!cfg->add(envOverride.path, std::move(node)))
+			LOG(Configuration, Error)
+				<< "Failed to override "
+				<< utils::join(envOverride.path, "/");
+	}
 }
 
 void GlobalConfiguration::load()
@@ -188,65 +301,4 @@ std::optional<std::vector<std::string>> GlobalConfiguration::listOption(
 	return c->get<std::vector<std::string>>();
 }
 
-/**
- * \brief Retrieve the value of environment variable with a fallback on the configuration file
- * \param[in] envVariable Environment variable to get the value from
- * \param[in] confPath The sequence of YAML section names to fall back on when
- * \a envVariable is unavailable
- *
- * This helper looks first at the given environment variable and if it is
- * defined then it returns its value (even if it is empty). Otherwise it looks
- * for \a confPath the same way as in GlobalConfiguration::option. Only string
- * values are supported.
- *
- * \note Support for using environment variables to configure libcamera behavior
- * is provided here mostly for backward compatibility reasons. Introducing new
- * configuration environment variables is discouraged.
- *
- * \return The value retrieved from the given environment if it is set,
- * otherwise the value from the configuration file if it exists, or no value if
- * it does not
- */
-std::optional<std::string> GlobalConfiguration::envOption(
-	const char *envVariable,
-	const std::initializer_list<std::string_view> confPath) const
-{
-	const char *envValue = utils::secure_getenv(envVariable);
-	if (envValue)
-		return std::optional{ std::string{ envValue } };
-	return option<std::string>(confPath);
-}
-
-/**
- * \brief Retrieve the value of the configuration option from a file or environment
- * \param[in] envVariable Environment variable to get the value from
- * \param[in] confPath The same as in GlobalConfiguration::option
- * \param[in] delimiter Items separator in the environment variable
- *
- * This helper looks first at the given environment variable and if it is
- * defined (even if it is empty) then it splits its value by semicolons and
- * returns the resulting list of strings. Otherwise it looks for \a confPath the
- * same way as in GlobalConfiguration::option, value of which must be a list of
- * strings.
- *
- * \note Support for using environment variables to configure libcamera behavior
- * is provided here mostly for backward compatibility reasons. Introducing new
- * configuration environment variables is discouraged.
- *
- * \return A vector of strings retrieved from the given environment option or
- * configuration file or no value if not found; the vector may be empty
- */
-std::optional<std::vector<std::string>> GlobalConfiguration::envListOption(
-	const char *const envVariable,
-	const std::initializer_list<std::string_view> confPath,
-	const std::string delimiter) const
-{
-	const char *envValue = utils::secure_getenv(envVariable);
-	if (envValue) {
-		auto items = utils::split(envValue, delimiter);
-		return std::vector<std::string>(items.begin(), items.end());
-	}
-	return listOption(confPath);
-}
-
 } /* namespace libcamera */
diff --git a/src/libcamera/ipa_manager.cpp b/src/libcamera/ipa_manager.cpp
index dd1f483beec3..a351f4f7b581 100644
--- a/src/libcamera/ipa_manager.cpp
+++ b/src/libcamera/ipa_manager.cpp
@@ -114,18 +114,15 @@ IPAManager::IPAManager(const CameraManager &cm)
 	if (!pubKey_.isValid())
 		LOG(IPAManager, Warning) << "Public key not valid";
 
-	char *force = utils::secure_getenv("LIBCAMERA_IPA_FORCE_ISOLATION");
-	forceIsolation_ = (force && force[0] != '\0') ||
-			  (!force && configuration.option<bool>({ "ipa", "force_isolation" })
-					     .value_or(false));
+	forceIsolation_ = configuration.option<bool>({ "ipa", "force_isolation" })
+				       .value_or(false);
 #endif
 
 	unsigned int ipaCount = 0;
 
 	/* User-specified paths take precedence. */
 	const auto modulePaths =
-		configuration.envListOption(
-				     "LIBCAMERA_IPA_MODULE_PATH", { "ipa", "module_paths" })
+		configuration.listOption({ "ipa", "module_paths" })
 			.value_or(std::vector<std::string>());
 	for (const auto &dir : modulePaths) {
 		if (dir.empty())
diff --git a/src/libcamera/ipa_proxy.cpp b/src/libcamera/ipa_proxy.cpp
index 6c8780a012d5..bc8ff090fa86 100644
--- a/src/libcamera/ipa_proxy.cpp
+++ b/src/libcamera/ipa_proxy.cpp
@@ -124,11 +124,9 @@ IPAProxy::IPAProxy(IPAModule *ipam, const CameraManager &cm)
 {
 	const GlobalConfiguration &configuration = cm._d()->configuration();
 
-	configPaths_ = configuration.envListOption("LIBCAMERA_IPA_CONFIG_PATH",
-						   { "ipa", "config_paths" })
+	configPaths_ = configuration.listOption({ "ipa", "config_paths" })
 				    .value_or(std::vector<std::string>());
-	execPaths_ = configuration.envListOption("LIBCAMERA_IPA_PROXY_PATH",
-						 { "ipa", "proxy_paths" })
+	execPaths_ = configuration.listOption({ "ipa", "proxy_paths" })
 				  .value_or(std::vector<std::string>());
 }
 
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index cd0e9d06a1e1..d227bd8e325f 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -106,11 +106,11 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 
 #if HAVE_DEBAYER_EGL
 	const GlobalConfiguration &configuration = cm._d()->configuration();
-	std::optional<std::string> softISPMode = configuration.envOption("LIBCAMERA_SOFTISP_MODE", { "software_isp", "mode" });
+	std::optional<std::string> softISPMode = configuration.option<std::string>({ "software_isp", "mode" });
 	if (softISPMode) {
 		if (softISPMode != "gpu" && softISPMode != "cpu") {
 			LOG(SoftwareIsp, Error)
-				<< "Invalid LIBCAMERA_SOFTISP_MODE \""
+				<< "Invalid software ISP mode \""
 				<< softISPMode.value()
 				<< "\", must be \"cpu\" or \"gpu\"";
 			return;
