diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c
index 1096b0b071..8631f357a7 100644
--- a/src/libostree/ostree-sysroot-deploy.c
+++ b/src/libostree/ostree-sysroot-deploy.c
@@ -86,15 +86,6 @@ symlink_at_replace (const char    *oldpath,
   return TRUE;
 }
 
-static GLnxFileCopyFlags
-sysroot_flags_to_copy_flags (GLnxFileCopyFlags defaults,
-                             OstreeSysrootDebugFlags sysrootflags)
-{
-  if (sysrootflags & OSTREE_SYSROOT_DEBUG_NO_XATTRS)
-    defaults |= GLNX_FILE_COPY_NOXATTRS;
-  return defaults;
-}
-
 /* Try a hardlink if we can, otherwise fall back to copying.  Used
  * right now for kernels/initramfs/device trees in /boot, where we can just
  * hardlink if we're on the same partition.
@@ -139,101 +130,6 @@ install_into_boot (OstreeSePolicy *sepolicy,
   return TRUE;
 }
 
-/* Copy ownership, mode, and xattrs from source directory to destination */
-static gboolean
-dirfd_copy_attributes_and_xattrs (int            src_parent_dfd,
-                                  const char    *src_name,
-                                  int            src_dfd,
-                                  int            dest_dfd,
-                                  OstreeSysrootDebugFlags flags,
-                                  GCancellable  *cancellable,
-                                  GError       **error)
-{
-  g_autoptr(GVariant) xattrs = NULL;
-
-  /* Clone all xattrs first, so we get the SELinux security context
-   * right.  This will allow other users access if they have ACLs, but
-   * oh well.
-   */
-  if (!(flags & OSTREE_SYSROOT_DEBUG_NO_XATTRS))
-    {
-      if (!glnx_dfd_name_get_all_xattrs (src_parent_dfd, src_name,
-                                         &xattrs, cancellable, error))
-        return FALSE;
-      if (!glnx_fd_set_all_xattrs (dest_dfd, xattrs,
-                                   cancellable, error))
-        return FALSE;
-    }
-
-  struct stat src_stbuf;
-  if (!glnx_fstat (src_dfd, &src_stbuf, error))
-    return FALSE;
-  if (fchown (dest_dfd, src_stbuf.st_uid, src_stbuf.st_gid) != 0)
-    return glnx_throw_errno_prefix (error, "fchown");
-  if (fchmod (dest_dfd, src_stbuf.st_mode) != 0)
-    return glnx_throw_errno_prefix (error, "fchmod");
-
-  return TRUE;
-}
-
-static gboolean
-copy_dir_recurse (int              src_parent_dfd,
-                  int              dest_parent_dfd,
-                  const char      *name,
-                  OstreeSysrootDebugFlags flags,
-                  GCancellable    *cancellable,
-                  GError         **error)
-{
-  g_auto(GLnxDirFdIterator) src_dfd_iter = { 0, };
-  glnx_autofd int dest_dfd = -1;
-  struct dirent *dent;
-
-  if (!glnx_dirfd_iterator_init_at (src_parent_dfd, name, TRUE, &src_dfd_iter, error))
-    return FALSE;
-
-  /* Create with mode 0700, we'll fchmod/fchown later */
-  if (!glnx_ensure_dir (dest_parent_dfd, name, 0700, error))
-    return FALSE;
-
-  if (!glnx_opendirat (dest_parent_dfd, name, TRUE, &dest_dfd, error))
-    return FALSE;
-
-  if (!dirfd_copy_attributes_and_xattrs (src_parent_dfd, name, src_dfd_iter.fd, dest_dfd,
-                                         flags, cancellable, error))
-    return FALSE;
-
-  while (TRUE)
-    {
-      struct stat child_stbuf;
-
-      if (!glnx_dirfd_iterator_next_dent (&src_dfd_iter, &dent, cancellable, error))
-        return FALSE;
-      if (dent == NULL)
-        break;
-
-      if (!glnx_fstatat (src_dfd_iter.fd, dent->d_name, &child_stbuf,
-                         AT_SYMLINK_NOFOLLOW, error))
-        return FALSE;
-
-      if (S_ISDIR (child_stbuf.st_mode))
-        {
-          if (!copy_dir_recurse (src_dfd_iter.fd, dest_dfd, dent->d_name,
-                                 flags, cancellable, error))
-            return FALSE;
-        }
-      else
-        {
-          if (!glnx_file_copy_at (src_dfd_iter.fd, dent->d_name, &child_stbuf,
-                                  dest_dfd, dent->d_name,
-                                  sysroot_flags_to_copy_flags (GLNX_FILE_COPY_OVERWRITE, flags),
-                                  cancellable, error))
-            return FALSE;
-        }
-    }
-
-  return TRUE;
-}
-
 /* If a chain of directories is added, this function will ensure
  * they're created.
  */
@@ -290,8 +186,8 @@ ensure_directory_from_template (int                 orig_etc_fd,
   if (!glnx_opendirat (new_etc_fd, path, TRUE, &target_dfd, error))
     return FALSE;
 
-  if (!dirfd_copy_attributes_and_xattrs (modified_etc_fd, path, src_dfd, target_dfd,
-                                         flags, cancellable, error))
+  if (!ot_dirfd_copy_attributes_and_xattrs (modified_etc_fd, path, src_dfd, target_dfd,
+                                            flags, cancellable, error))
     return FALSE;
 
   if (out_dfd)
@@ -365,15 +261,15 @@ copy_modified_config_file (int                 orig_etc_fd,
 
   if (S_ISDIR (modified_stbuf.st_mode))
     {
-      if (!copy_dir_recurse (modified_etc_fd, new_etc_fd, path, flags,
-                             cancellable, error))
+      if (!ot_copy_dir_recurse (modified_etc_fd, new_etc_fd, path, flags,
+                                cancellable, error))
         return FALSE;
     }
   else if (S_ISLNK (modified_stbuf.st_mode) || S_ISREG (modified_stbuf.st_mode))
     {
       if (!glnx_file_copy_at (modified_etc_fd, path, &modified_stbuf,
                               new_etc_fd, path,
-                              sysroot_flags_to_copy_flags (GLNX_FILE_COPY_OVERWRITE, flags),
+                              ot_sysroot_flags_to_copy_flags (GLNX_FILE_COPY_OVERWRITE, flags),
                               cancellable, error))
         return FALSE;
     }
diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h
index e4b2039e2f..eef50160ac 100644
--- a/src/libostree/ostree-sysroot-private.h
+++ b/src/libostree/ostree-sysroot-private.h
@@ -28,19 +28,6 @@
 
 G_BEGIN_DECLS
 
-typedef enum {
-
-  /* Don't flag deployments as immutable. */
-  OSTREE_SYSROOT_DEBUG_MUTABLE_DEPLOYMENTS = 1 << 0,
-  /* See https://github.com/ostreedev/ostree/pull/759 */
-  OSTREE_SYSROOT_DEBUG_NO_XATTRS = 1 << 1,
-  /* https://github.com/ostreedev/ostree/pull/1049 */
-  OSTREE_SYSROOT_DEBUG_TEST_FIFREEZE = 1 << 2,
-  /* This is a temporary flag until we fully drop the explicit `systemctl start
-   * ostree-finalize-staged.service` so that tests can exercise the new path unit. */
-  OSTREE_SYSROOT_DEBUG_TEST_STAGED_PATH = 1 << 3,
-} OstreeSysrootDebugFlags;
-
 /**
  * OstreeSysroot:
  * Internal struct
diff --git a/src/libotutil/ot-fs-utils.c b/src/libotutil/ot-fs-utils.c
index c4fcd56f11..fac408a95e 100644
--- a/src/libotutil/ot-fs-utils.c
+++ b/src/libotutil/ot-fs-utils.c
@@ -247,3 +247,98 @@ ot_parse_file_by_line (const char    *path,
 
   return TRUE;
 }
+
+/* Copy ownership, mode, and xattrs from source directory to destination */
+gboolean
+ot_dirfd_copy_attributes_and_xattrs (int            src_parent_dfd,
+                                     const char    *src_name,
+                                     int            src_dfd,
+                                     int            dest_dfd,
+                                     OstreeSysrootDebugFlags flags,
+                                     GCancellable  *cancellable,
+                                     GError       **error)
+{
+  g_autoptr(GVariant) xattrs = NULL;
+
+  /* Clone all xattrs first, so we get the SELinux security context
+   * right.  This will allow other users access if they have ACLs, but
+   * oh well.
+   */
+  if (!(flags & OSTREE_SYSROOT_DEBUG_NO_XATTRS))
+    {
+      if (!glnx_dfd_name_get_all_xattrs (src_parent_dfd, src_name,
+                                         &xattrs, cancellable, error))
+        return FALSE;
+      if (!glnx_fd_set_all_xattrs (dest_dfd, xattrs,
+                                   cancellable, error))
+        return FALSE;
+    }
+
+  struct stat src_stbuf;
+  if (!glnx_fstat (src_dfd, &src_stbuf, error))
+    return FALSE;
+  if (fchown (dest_dfd, src_stbuf.st_uid, src_stbuf.st_gid) != 0)
+    return glnx_throw_errno_prefix (error, "fchown");
+  if (fchmod (dest_dfd, src_stbuf.st_mode) != 0)
+    return glnx_throw_errno_prefix (error, "fchmod");
+
+  return TRUE;
+}
+
+gboolean
+ot_copy_dir_recurse (int              src_parent_dfd,
+                     int              dest_parent_dfd,
+                     const char      *name,
+                     OstreeSysrootDebugFlags flags,
+                     GCancellable    *cancellable,
+                     GError         **error)
+{
+  g_auto(GLnxDirFdIterator) src_dfd_iter = { 0, };
+  glnx_autofd int dest_dfd = -1;
+  struct dirent *dent;
+
+  if (!glnx_dirfd_iterator_init_at (src_parent_dfd, name, TRUE, &src_dfd_iter, error))
+    return FALSE;
+
+  /* Create with mode 0700, we'll fchmod/fchown later */
+  if (!glnx_ensure_dir (dest_parent_dfd, name, 0700, error))
+    return FALSE;
+
+  if (!glnx_opendirat (dest_parent_dfd, name, TRUE, &dest_dfd, error))
+    return FALSE;
+
+  if (!ot_dirfd_copy_attributes_and_xattrs (src_parent_dfd, name, src_dfd_iter.fd, dest_dfd,
+                                            flags, cancellable, error))
+    return FALSE;
+
+  while (TRUE)
+    {
+      struct stat child_stbuf;
+
+      if (!glnx_dirfd_iterator_next_dent (&src_dfd_iter, &dent, cancellable, error))
+        return FALSE;
+      if (dent == NULL)
+        break;
+
+      if (!glnx_fstatat (src_dfd_iter.fd, dent->d_name, &child_stbuf,
+                         AT_SYMLINK_NOFOLLOW, error))
+        return FALSE;
+
+      if (S_ISDIR (child_stbuf.st_mode))
+        {
+          if (!ot_copy_dir_recurse (src_dfd_iter.fd, dest_dfd, dent->d_name,
+                                    flags, cancellable, error))
+            return FALSE;
+        }
+      else
+        {
+          if (!glnx_file_copy_at (src_dfd_iter.fd, dent->d_name, &child_stbuf,
+                                  dest_dfd, dent->d_name,
+                                  ot_sysroot_flags_to_copy_flags (GLNX_FILE_COPY_OVERWRITE, flags),
+                                  cancellable, error))
+            return FALSE;
+        }
+    }
+
+  return TRUE;
+}
diff --git a/src/libotutil/ot-fs-utils.h b/src/libotutil/ot-fs-utils.h
index 74a0fed6d8..225676d833 100644
--- a/src/libotutil/ot-fs-utils.h
+++ b/src/libotutil/ot-fs-utils.h
@@ -26,6 +26,19 @@
 
 G_BEGIN_DECLS
 
+typedef enum {
+
+  /* Don't flag deployments as immutable. */
+  OSTREE_SYSROOT_DEBUG_MUTABLE_DEPLOYMENTS = 1 << 0,
+  /* See https://github.com/ostreedev/ostree/pull/759 */
+  OSTREE_SYSROOT_DEBUG_NO_XATTRS = 1 << 1,
+  /* https://github.com/ostreedev/ostree/pull/1049 */
+  OSTREE_SYSROOT_DEBUG_TEST_FIFREEZE = 1 << 2,
+  /* This is a temporary flag until we fully drop the explicit `systemctl start
+   * ostree-finalize-staged.service` so that tests can exercise the new path unit. */
+  OSTREE_SYSROOT_DEBUG_TEST_STAGED_PATH = 1 << 3,
+} OstreeSysrootDebugFlags;
+
 /* A little helper to call unlinkat() as a cleanup
  * function.  Mostly only necessary to handle
  * deletion of temporary symlinks.
@@ -52,6 +65,15 @@ ot_cleanup_unlinkat (OtCleanupUnlinkat *cleanup)
 }
 G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(OtCleanupUnlinkat, ot_cleanup_unlinkat)
 
+static inline GLnxFileCopyFlags
+ot_sysroot_flags_to_copy_flags (GLnxFileCopyFlags defaults,
+                                OstreeSysrootDebugFlags sysrootflags)
+{
+  if (sysrootflags & OSTREE_SYSROOT_DEBUG_NO_XATTRS)
+    defaults |= GLNX_FILE_COPY_NOXATTRS;
+  return defaults;
+}
+
 GFile * ot_fdrel_to_gfile (int dfd, const char *path);
 
 gboolean ot_readlinkat_gfile_info (int             dfd,
@@ -97,4 +119,21 @@ ot_parse_file_by_line (const char    *path,
                        GCancellable  *cancellable,
                        GError       **error);
 
+gboolean
+ot_dirfd_copy_attributes_and_xattrs (int            src_parent_dfd,
+                                     const char    *src_name,
+                                     int            src_dfd,
+                                     int            dest_dfd,
+                                     OstreeSysrootDebugFlags flags,
+                                     GCancellable  *cancellable,
+                                     GError       **error);
+
+gboolean
+ot_copy_dir_recurse (int              src_parent_dfd,
+                     int              dest_parent_dfd,
+                     const char      *name,
+                     OstreeSysrootDebugFlags flags,
+                     GCancellable    *cancellable,
+                     GError         **error);
+
 G_END_DECLS