diff --git a/api/src/main/java/run/halo/app/extension/Secret.java b/api/src/main/java/run/halo/app/extension/Secret.java
new file mode 100644
index 000000000..ce029d807
--- /dev/null
+++ b/api/src/main/java/run/halo/app/extension/Secret.java
@@ -0,0 +1,54 @@
+package run.halo.app.extension;
+
+import java.util.Map;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * Secret is a small piece of sensitive data which should be kept secret, such as a password,
+ * a token, or a key.
+ *
+ * @author guqing
+ * @see
+ * kebernetes Secret
+ * @since 2.0.0
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@GVK(group = "", version = "v1alpha1", kind = Secret.KIND, plural = "secrets", singular = "secret")
+public class Secret extends AbstractExtension {
+ public static final String KIND = "Secret";
+
+ public static final String SECRET_TYPE_OPAQUE = "Opaque";
+
+ public static final int MAX_SECRET_SIZE = 1024 * 1024;
+
+ /**
+ * Used to facilitate programmatic handling of secret data.
+ * More info:
+ * secret-types
+ */
+ private String type;
+
+ /**
+ *
The total bytes of the values in
+ * the Data field must be less than {@link #MAX_SECRET_SIZE} bytes.
+ * {@code data} contains the secret data. Each key must consist of alphanumeric
+ * characters, '-', '_' or '.'. The serialized form of the secret data is a
+ * base64 encoded string, representing the arbitrary (possibly non-string)
+ * data value here. Described in
+ * rfc4648#section-4
+ *
+ */
+ private Map data;
+
+ /**
+ * {@code stringData} allows specifying non-binary secret data in string form.
+ * It is provided as a write-only input field for convenience.
+ * All keys and values are merged into the data field on write, overwriting any existing
+ * values.
+ * The stringData field is never output when reading from the API.
+ */
+ private Map stringData;
+
+}
diff --git a/api/src/test/java/run/halo/app/extension/SecretTest.java b/api/src/test/java/run/halo/app/extension/SecretTest.java
new file mode 100644
index 000000000..af18827f7
--- /dev/null
+++ b/api/src/test/java/run/halo/app/extension/SecretTest.java
@@ -0,0 +1,84 @@
+package run.halo.app.extension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import java.util.Map;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import run.halo.app.infra.utils.JsonUtils;
+
+/**
+ * Tests for {@link Secret}.
+ *
+ * @author guqing
+ * @since 2.4.0
+ */
+class SecretTest {
+
+ @Test
+ void serialize() throws JSONException {
+ Secret secret = new Secret();
+ secret.setMetadata(new Metadata());
+ secret.getMetadata().setName("test-secret");
+ secret.setType(Secret.SECRET_TYPE_OPAQUE);
+ secret.setData(Map.of("password", "admin".getBytes()));
+ String s = JsonUtils.objectToJson(secret);
+ JSONAssert.assertEquals(testJsonString(), s, true);
+ }
+
+ @Test
+ void deserialize() {
+ String s = testJsonString();
+ Secret secret = JsonUtils.jsonToObject(s, Secret.class);
+ assertThat(secret).isNotNull();
+ assertThat(secret.getMetadata().getName()).isEqualTo("test-secret");
+ assertThat(secret.getType()).isEqualTo(Secret.SECRET_TYPE_OPAQUE);
+ assertThat(secret.getData()).containsEntry("password", "admin".getBytes());
+ }
+
+ @Test
+ void deserializeWithUnstructured() throws JsonProcessingException {
+ Secret secret = Unstructured.OBJECT_MAPPER.readValue(testJsonString(), Secret.class);
+ assertThat(secret.getMetadata().getName()).isEqualTo("test-secret");
+ assertThat(secret.getType()).isEqualTo(Secret.SECRET_TYPE_OPAQUE);
+ assertThat(secret.getData()).containsEntry("password", "admin".getBytes());
+ }
+
+ @Test
+ void deserializeYamlWithStringData() throws JsonProcessingException {
+ String s = """
+ apiVersion: v1alpha1
+ kind: Secret
+ metadata:
+ name: secret-basic-auth
+ type: halo.run/basic-auth
+ stringData:
+ username: admin
+ password: t0p-Secret
+ """;
+ Secret secret = new YAMLMapper().readValue(s, Secret.class);
+ assertThat(secret.getMetadata().getName()).isEqualTo("secret-basic-auth");
+ assertThat(secret.getType()).isEqualTo("halo.run/basic-auth");
+ assertThat(secret.getStringData()).containsEntry("username", "admin");
+ assertThat(secret.getStringData()).containsEntry("password", "t0p-Secret");
+ }
+
+ private String testJsonString() {
+ return """
+ {
+ "apiVersion": "v1alpha1",
+ "kind": "Secret",
+ "metadata": {
+ "name": "test-secret"
+ },
+ "type": "Opaque",
+ "data": {
+ "password": "YWRtaW4="
+ }
+ }
+ """;
+ }
+}
diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java
index 326e27108..ce5f21ba8 100644
--- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java
+++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java
@@ -31,6 +31,7 @@ import run.halo.app.core.extension.content.Snapshot;
import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.SchemeManager;
+import run.halo.app.extension.Secret;
import run.halo.app.plugin.extensionpoint.ExtensionDefinition;
import run.halo.app.plugin.extensionpoint.ExtensionPointDefinition;
import run.halo.app.search.extension.SearchEngine;
@@ -66,6 +67,7 @@ public class SchemeInitializer implements ApplicationListener