diff --git a/include/libcamera/internal/global_configuration.h b/include/libcamera/internal/global_configuration.h
new file mode 100644
index 00000000..2f38056a
--- /dev/null
+++ b/include/libcamera/internal/global_configuration.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, inc.
+ *
+ * global_configuration.h - Global configuration handling
+ */
+
+#pragma once
+
+#include <filesystem>
+#include <vector>
+
+#include "libcamera/internal/yaml_parser.h"
+
+namespace libcamera {
+
+class GlobalConfiguration
+{
+public:
+	using Configuration = const YamlObject &;
+
+	static unsigned int version();
+	static Configuration configuration();
+
+private:
+	static const std::vector<std::filesystem::path> globalConfigurationFiles;
+
+	bool initialized_;
+	std::unique_ptr<YamlObject> configuration_;
+
+	GlobalConfiguration();
+	bool loadFile(const std::filesystem::path &fileName);
+	void load();
+	static const GlobalConfiguration &instance();
+	static Configuration get();
+};
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
index 160fdc37..640a54aa 100644
--- a/include/libcamera/internal/meson.build
+++ b/include/libcamera/internal/meson.build
@@ -28,6 +28,7 @@ libcamera_internal_headers = files([
     'dma_heaps.h',
     'formats.h',
     'framebuffer.h',
+    'global_configuration.h',
     'ipa_manager.h',
     'ipa_module.h',
     'ipa_proxy.h',
diff --git a/src/libcamera/global_configuration.cpp b/src/libcamera/global_configuration.cpp
new file mode 100644
index 00000000..96088b36
--- /dev/null
+++ b/src/libcamera/global_configuration.cpp
@@ -0,0 +1,163 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, inc.
+ *
+ * global_configuration.cpp - Global configuration handling
+ */
+
+#include "libcamera/internal/global_configuration.h"
+
+#include <filesystem>
+#include <memory>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Configuration)
+
+/**
+ * \class GlobalConfiguration
+ * \brief Support for global libcamera configuration
+ *
+ * The configuration file is a YAML file and the configuration itself is stored
+ * under `configuration' top-level item.
+ *
+ * The configuration file is looked up in user's home directory first and if it
+ * is not found then in system-wide configuration directories. If multiple
+ * configuration files exist then only the first one found is used and no
+ * configuration merging is performed.
+ *
+ * The class is used as a private singleton and the configuration can be
+ * accessed usingGlobalConfiguration::configuration().
+ */
+
+/**
+ * \typedef GlobalConfiguration::Configuration
+ * \brief Type representing global libcamera configuration
+ *
+ * All code outside GlobalConfiguration must use this type declaration and not
+ * the underlying type.
+ */
+
+GlobalConfiguration::GlobalConfiguration()
+	: initialized_(false), configuration_(std::make_unique<YamlObject>())
+{
+}
+
+const std::vector<std::filesystem::path>
+	GlobalConfiguration::globalConfigurationFiles = {
+		std::filesystem::path(LIBCAMERA_SYSCONF_DIR) / "configuration.yaml",
+		std::filesystem::path("/etc/libcamera/configuration.yaml"),
+	};
+
+void GlobalConfiguration::load()
+{
+	std::filesystem::path userConfigurationDirectory;
+	char *xdgConfigHome = utils::secure_getenv("XDG_CONFIG_HOME");
+	if (xdgConfigHome) {
+		userConfigurationDirectory = xdgConfigHome;
+	} else {
+		const char *home = utils::secure_getenv("HOME");
+		if (home)
+			userConfigurationDirectory =
+				std::filesystem::path(home) / ".config";
+	}
+
+	if (!userConfigurationDirectory.empty()) {
+		std::filesystem::path user_configuration_file =
+			userConfigurationDirectory / "libcamera" / "configuration.yaml";
+		if (loadFile(user_configuration_file))
+			return;
+	}
+
+	for (auto path : globalConfigurationFiles)
+		if (loadFile(path))
+			return;
+
+	initialized_ = true;
+	LOG(Configuration, Debug) << "No configuration file found";
+}
+
+bool GlobalConfiguration::loadFile(const std::filesystem::path &fileName)
+{
+	File file(fileName);
+	if (!file.exists()) {
+		return false;
+	}
+
+	if (!file.open(File::OpenModeFlag::ReadOnly)) {
+		LOG(Configuration, Warning)
+			<< "Failed to open configuration file '" << fileName << "'";
+		return true;
+	}
+
+	auto root = YamlParser::parse(file);
+	if (!root) {
+		LOG(Configuration, Warning) << "Failed to parse configuration file "
+					    << fileName;
+		return true;
+	}
+	configuration_ = std::move(root);
+	initialized_ = true;
+	LOG(Configuration, Info) << "Configuration file " << fileName << "loaded";
+
+	return true;
+}
+
+const GlobalConfiguration &GlobalConfiguration::instance()
+{
+	static GlobalConfiguration configuration;
+	if (!configuration.initialized_) {
+		configuration.load();
+	}
+	return configuration;
+}
+
+GlobalConfiguration::Configuration GlobalConfiguration::get()
+{
+	return (*instance().configuration_);
+}
+
+/**
+ * \brief Return configuration version
+ *
+ * The version is (optionally) declared in the configuration file in the
+ * top-level section `version', alongside `configuration'. This has currently no
+ * real use but may be needed in future if configuration incompatibilities
+ * occur.
+ *
+ * \return Configuration version as declared in the configuration file or 0 if
+ * no version is declared there
+ */
+unsigned int GlobalConfiguration::version()
+{
+	return get()["version"].get<unsigned int>().value_or(0);
+}
+
+/**
+ * \brief Return libcamera global configuration
+ *
+ * This returns the whole configuration stored in the top-level section
+ * `configuration' of the YAML configuration file.
+ *
+ * The requested part of the configuration can be accessed using \a YamlObject
+ * methods.
+ *
+ * \note \a YamlObject type itself shouldn't be used in type declarations to
+ * avoid trouble if we decide to change the underlying data objects in future.
+ *
+ * \return The whole configuration section
+ */
+GlobalConfiguration::Configuration GlobalConfiguration::configuration()
+{
+	return get()["configuration"];
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index a3b12bc1..b81c75b8 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -21,6 +21,7 @@ libcamera_sources = files([
     'framebuffer.cpp',
     'framebuffer_allocator.cpp',
     'geometry.cpp',
+    'global_configuration.cpp',
     'ipa_controls.cpp',
     'ipa_data_serializer.cpp',
     'ipa_interface.cpp',
