diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h
index d676ed2b246e..385e134277b1 100644
--- a/include/uapi/linux/audit.h
+++ b/include/uapi/linux/audit.h
@@ -122,6 +122,7 @@
 #define AUDIT_OPENAT2		1337	/* Record showing openat2 how args */
 #define AUDIT_DM_CTRL		1338	/* Device Mapper target control */
 #define AUDIT_DM_EVENT		1339	/* Device Mapper events */
+#define AUDIT_LANDLOCK		1340	/* Landlock event */
 
 #define AUDIT_AVC		1400	/* SE Linux avc denial or grant */
 #define AUDIT_SELINUX_ERR	1401	/* Internal SE Linux Errors */
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index 7bbd2f413b3e..c3e048df7fec 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -2,3 +2,5 @@ obj-$(CONFIG_SECURITY_LANDLOCK) := landlock.o
 
 landlock-y := setup.o syscalls.o object.o ruleset.o \
 	cred.o ptrace.o fs.o
+
+landlock-$(CONFIG_AUDIT) += audit.o
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
new file mode 100644
index 000000000000..f58bd529784a
--- /dev/null
+++ b/security/landlock/audit.c
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock LSM - Audit helpers
+ *
+ * Copyright © 2023 Microsoft Corporation
+ */
+
+#include <linux/atomic.h>
+#include <linux/audit.h>
+#include <linux/lsm_audit.h>
+
+#include "audit.h"
+#include "cred.h"
+
+atomic64_t ruleset_and_domain_counter = ATOMIC64_INIT(0);
+
+#define BIT_INDEX(bit) HWEIGHT(bit - 1)
+
+static void log_accesses(struct audit_buffer *const ab,
+			 const access_mask_t accesses)
+{
+	const char *const desc[] = {
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "execute",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "write_file",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "read_file",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "read_dir",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "remove_dir",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "remove_file",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "make_char",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "make_dir",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "make_reg",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "make_sock",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "make_fifo",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "make_block",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "make_sym",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "refer",
+		[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "truncate",
+	};
+	const unsigned long access_mask = accesses;
+	unsigned long access_bit;
+	bool is_first = true;
+
+	BUILD_BUG_ON(ARRAY_SIZE(desc) != LANDLOCK_NUM_ACCESS_FS);
+
+	for_each_set_bit(access_bit, &access_mask, ARRAY_SIZE(desc)) {
+		audit_log_format(ab, "%s%s", is_first ? "" : ",",
+				 desc[access_bit]);
+		is_first = false;
+	}
+}
+
+/* Inspired by dump_common_audit_data(). */
+static void log_task(struct audit_buffer *const ab)
+{
+	/* 16 bytes (TASK_COMM_LEN) */
+	char comm[sizeof(current->comm)];
+
+	/*
+	 * Uses task_pid_nr() instead of task_tgid_nr() because of how
+	 * credentials and Landlock work.
+	 */
+	audit_log_format(ab, "tid=%d comm=", task_pid_nr(current));
+	audit_log_untrustedstring(ab,
+				  memcpy(comm, current->comm, sizeof(comm)));
+}
+
+void landlock_log_create_ruleset(struct landlock_ruleset *const ruleset)
+{
+	struct audit_buffer *ab;
+
+	WARN_ON_ONCE(ruleset->id);
+
+	ab = audit_log_start(audit_context(), GFP_ATOMIC, AUDIT_LANDLOCK);
+	if (!ab)
+		/* audit_log_lost() call */
+		return;
+
+	ruleset->id = atomic64_inc_return(&ruleset_and_domain_counter);
+	log_task(ab);
+	audit_log_format(ab,
+			 " op=create-ruleset ruleset=%llu handled_access_fs=",
+			 ruleset->id);
+	log_accesses(ab, ruleset->fs_access_masks[ruleset->num_layers - 1]);
+	audit_log_end(ab);
+}
+
+/*
+ * This is useful to know when a domain or a ruleset will never show again in
+ * the audit log.
+ */
+void landlock_log_release_ruleset(const struct landlock_ruleset *const ruleset)
+{
+	struct audit_buffer *ab;
+	const char *name;
+	u64 id;
+
+	ab = audit_log_start(audit_context(), GFP_ATOMIC, AUDIT_LANDLOCK);
+	if (!ab)
+		/* audit_log_lost() call */
+		return;
+
+	/* It should either be a domain or a ruleset. */
+	if (ruleset->hierarchy) {
+		name = "domain";
+		id = ruleset->hierarchy->id;
+		WARN_ON_ONCE(ruleset->id);
+	} else {
+		name = "ruleset";
+		id = ruleset->id;
+	}
+	WARN_ON_ONCE(!id);
+
+	/*
+	 * Because this might be called by kernel threads, logging
+	 * related task information with log_task() would be useless.
+	 */
+	audit_log_format(ab, "op=release-%s %s=%llu", name, name, id);
+	audit_log_end(ab);
+}
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
new file mode 100644
index 000000000000..2666e9151627
--- /dev/null
+++ b/security/landlock/audit.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock LSM - Audit helpers
+ *
+ * Copyright © 2023 Microsoft Corporation
+ */
+
+#ifndef _SECURITY_LANDLOCK_AUDIT_H
+#define _SECURITY_LANDLOCK_AUDIT_H
+
+#include <linux/audit.h>
+#include <linux/lsm_audit.h>
+
+#include "ruleset.h"
+
+#ifdef CONFIG_AUDIT
+
+void landlock_log_create_ruleset(struct landlock_ruleset *const ruleset);
+void landlock_log_release_ruleset(const struct landlock_ruleset *const ruleset);
+
+#else /* CONFIG_AUDIT */
+
+static inline void
+landlock_log_create_ruleset(struct landlock_ruleset *const ruleset)
+{
+}
+
+static inline void
+landlock_log_release_ruleset(const struct landlock_ruleset *const ruleset)
+{
+}
+
+#endif /* CONFIG_AUDIT */
+
+#endif /* _SECURITY_LANDLOCK_AUDIT_H */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 996484f98bfd..585ee0f77e67 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -20,6 +20,7 @@
 #include <linux/spinlock.h>
 #include <linux/workqueue.h>
 
+#include "audit.h"
 #include "limits.h"
 #include "object.h"
 #include "ruleset.h"
@@ -379,6 +380,11 @@ static void free_ruleset_work(struct work_struct *const work)
 	struct landlock_ruleset *ruleset;
 
 	ruleset = container_of(work, struct landlock_ruleset, work_free);
+
+	/* Only called by hook_cred_free(), hence for a domain. */
+	WARN_ON_ONCE(!ruleset->hierarchy);
+	landlock_log_release_ruleset(ruleset);
+
 	free_ruleset(ruleset);
 }
 
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 55b1df8f66a8..c74f1ab60c33 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -74,6 +74,11 @@ struct landlock_rule {
  * struct landlock_hierarchy - Node in a ruleset hierarchy
  */
 struct landlock_hierarchy {
+#ifdef CONFIG_AUDIT
+	/* domain's ID */
+	u64 id;
+#endif /* CONFIG_AUDIT */
+
 	/**
 	 * @parent: Pointer to the parent node, or NULL if it is a root
 	 * Landlock domain.
@@ -93,6 +98,11 @@ struct landlock_hierarchy {
  * match an object.
  */
 struct landlock_ruleset {
+#ifdef CONFIG_AUDIT
+	/* ruleset's ID, must be 0 for a domain */
+	u64 id;
+#endif /* CONFIG_AUDIT */
+
 	/**
 	 * @root: Root of a red-black tree containing &struct landlock_rule
 	 * nodes.  Once a ruleset is tied to a process (i.e. as a domain), this
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 245cc650a4dc..373997a356e7 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -26,6 +26,7 @@
 #include <linux/uaccess.h>
 #include <uapi/linux/landlock.h>
 
+#include "audit.h"
 #include "cred.h"
 #include "fs.h"
 #include "limits.h"
@@ -98,6 +99,10 @@ static int fop_ruleset_release(struct inode *const inode,
 {
 	struct landlock_ruleset *ruleset = filp->private_data;
 
+	/* Only called by ruleset_fops, hence for a ruleset. */
+	WARN_ON_ONCE(ruleset->hierarchy);
+	landlock_log_release_ruleset(ruleset);
+
 	landlock_put_ruleset(ruleset);
 	return 0;
 }
@@ -198,6 +203,9 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
 				      ruleset, O_RDWR | O_CLOEXEC);
 	if (ruleset_fd < 0)
 		landlock_put_ruleset(ruleset);
+	else
+		landlock_log_create_ruleset(ruleset);
+
 	return ruleset_fd;
 }
 
