diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
new file mode 100644
index 00000000..3c7fca2d
--- /dev/null
+++ b/src/ipa/simple/algorithms/ccm.cpp
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Color correction matrix
+ */
+
+#include "ccm.h"
+
+#include <stdlib.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+LOG_DEFINE_CATEGORY(IPASoftCcm)
+
+unsigned int Ccm::kTemperatureThreshold = 100;
+
+int Ccm::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData)
+{
+	int ret = ccm_.readYaml(tuningData["ccms"], "ct", "ccm");
+	if (ret < 0) {
+		LOG(IPASoftCcm, Warning)
+			<< "Failed to parse 'ccm' "
+			<< "parameter from tuning file; falling back to unit matrix";
+		ccmEnabled_ = false;
+	} else {
+		ccmEnabled_ = true;
+	}
+
+	return 0;
+}
+
+void Ccm::prepare(IPAContext &context, const uint32_t frame,
+		  IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)
+{
+	context.activeState.ccm.enabled = ccmEnabled_;
+
+	if (!ccmEnabled_)
+		return;
+
+	unsigned int ct = context.activeState.awb.temperatureK;
+
+	/* Change CCM only on bigger temperature changes. */
+	if (frame > 0 &&
+	    utils::abs_diff(ct, ct_) < kTemperatureThreshold) {
+		frameContext.ccm.ccm = context.activeState.ccm.ccm;
+		context.activeState.ccm.changed = false;
+		return;
+	}
+
+	ct_ = ct;
+	Matrix<double, 3, 3> ccm = ccm_.getInterpolated(ct);
+
+	context.activeState.ccm.ccm = ccm;
+	frameContext.ccm.ccm = ccm;
+	context.activeState.ccm.changed = true;
+}
+
+void Ccm::process([[maybe_unused]] IPAContext &context,
+		  [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  [[maybe_unused]] const SwIspStats *stats,
+		  ControlList &metadata)
+{
+	if (!ccmEnabled_)
+		return;
+
+	float m[9];
+	for (unsigned int i = 0; i < 3; i++) {
+		for (unsigned int j = 0; j < 3; j++)
+			m[i * 3 + j] = frameContext.ccm.ccm[i][j];
+	}
+	metadata.set(controls::ColourCorrectionMatrix, m);
+}
+
+REGISTER_IPA_ALGORITHM(Ccm, "Ccm")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/ccm.h b/src/ipa/simple/algorithms/ccm.h
new file mode 100644
index 00000000..23481a08
--- /dev/null
+++ b/src/ipa/simple/algorithms/ccm.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Color correction matrix
+ */
+
+#pragma once
+
+#include "libcamera/internal/matrix.h"
+
+#include <libipa/interpolator.h>
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class Ccm : public Algorithm
+{
+public:
+	Ccm() = default;
+	~Ccm() = default;
+
+	int init(IPAContext &context, const YamlObject &tuningData) override;
+	void prepare(IPAContext &context,
+		     const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     DebayerParams *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const SwIspStats *stats,
+		     ControlList &metadata) override;
+
+private:
+	static unsigned int kTemperatureThreshold;
+	unsigned int ct_;
+	bool ccmEnabled_;
+	Interpolator<Matrix<double, 3, 3>> ccm_;
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
index 37a2eb53..2d0adb05 100644
--- a/src/ipa/simple/algorithms/meson.build
+++ b/src/ipa/simple/algorithms/meson.build
@@ -4,5 +4,6 @@ soft_simple_ipa_algorithms = files([
     'awb.cpp',
     'agc.cpp',
     'blc.cpp',
+    'ccm.cpp',
     'lut.cpp',
 ])
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 607af45a..0def3eef 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -13,6 +13,8 @@
 
 #include <libcamera/controls.h>
 
+#include "libcamera/internal/matrix.h"
+
 #include <libipa/fc_queue.h>
 
 namespace libcamera {
@@ -50,6 +52,13 @@ struct IPAActiveState {
 		uint8_t blackLevel;
 		double contrast;
 	} gamma;
+
+	struct {
+		Matrix<double, 3, 3> ccm;
+		bool enabled;
+		bool changed;
+	} ccm;
+
 	struct {
 		/* 0..2 range, 1.0 = normal */
 		std::optional<double> contrast;
@@ -57,6 +66,10 @@ struct IPAActiveState {
 };
 
 struct IPAFrameContext : public FrameContext {
+	struct {
+		Matrix<double, 3, 3> ccm;
+	} ccm;
+
 	struct {
 		int32_t exposure;
 		double gain;
