diff --git a/Kconfig b/Kconfig
index 86a4aac3..66dd4112 100644
--- a/Kconfig
+++ b/Kconfig
@@ -14,7 +14,7 @@ config KAS_INCLUDE_MAIN
 
 config KAS_BUILD_SYSTEM
 	string
-	default "isar"
+	default "isar-rootless"
 
 source "kas/machine/Kconfig"
 source "kas/distro/Kconfig"
diff --git a/RECIPE-API-CHANGELOG.md b/RECIPE-API-CHANGELOG.md
index d0aa6e1a..27d14dc4 100644
--- a/RECIPE-API-CHANGELOG.md
+++ b/RECIPE-API-CHANGELOG.md
@@ -1077,3 +1077,24 @@ specifies the rootfs path.
 Using these helpers instead of direct `sudo` invocations centralizes platform-specific
 privileged execution logic in `base.bbclass`. Direct use of `sudo` is discouraged
 in downstream layers.
+
+### Rootless isar execution
+
+Isar is able to run without the need for `sudo` in an environment that
+allows unprivileged users to unshare the kernels `user namespace`. Further,
+a sufficiently large set of sub ids needs to be configured in `/etc/subuid` / `etc/subgid`.
+This range should be `> 65536`, but smaller ranges might work as well, depending on the
+ids used in the rootfs.
+
+A simple check if rootless is supported can be done by running:
+
+```bash
+mmdebstrap --unshare-helper /bin/echo "rootless supported" || echo "rootless not supported"
+```
+
+To enable rootless builds, set the bitbake variable `ISAR_ROOTLESS = "1"`.
+This internally switches the chroot mode from `schroot` to `unshare`.
+
+When using kas, the `build_system` needs to be set to `isar-rootless`, but the final
+interfaces still need to be clarified. Further, kas patches are needed (for details,
+check the kas mailing list).
diff --git a/doc/user_manual.md b/doc/user_manual.md
index 69e8dfef..26041f9a 100644
--- a/doc/user_manual.md
+++ b/doc/user_manual.md
@@ -74,6 +74,7 @@ Building `debian-trixie` requires host system >= bookworm.
 Install the following packages:
 ```
 apt install \
+  acl \
   binfmt-support \
   bubblewrap \
   bzip2 \
@@ -88,6 +89,7 @@ apt install \
   qemu-user-static \
   reprepro \
   sudo \
+  uidmap \
   unzip \
   xz-utils \
   git-buildpackage \
diff --git a/kas/isar.yaml b/kas/isar.yaml
index 16ce8b42..3cfc4f96 100644
--- a/kas/isar.yaml
+++ b/kas/isar.yaml
@@ -4,7 +4,7 @@
 header:
   version: 14
 
-build_system: isar
+build_system: isar-rootless
 
 repos:
   isar:
diff --git a/meta/classes-global/base.bbclass b/meta/classes-global/base.bbclass
index 90e4525e..7167cbb1 100644
--- a/meta/classes-global/base.bbclass
+++ b/meta/classes-global/base.bbclass
@@ -141,7 +141,9 @@ root_cleandirs() {
             die "Could not remove $i, because subdir is mounted"
     done
     for i in $ROOT_CLEANDIRS_DIRS; do
-        run_privileged rm -rf --one-file-system "$TMPDIR$i"
+        [ -d "$TMPDIR$i" ] || continue
+        find "$TMPDIR$i" \( ! -user "$(whoami)" -type d -prune \) -exec ${RUN_PRIVILEGED_CMD} rm -rf --one-file-system {} \;
+        rm -rf --one-file-system "$TMPDIR$i"
         mkdir -p "$TMPDIR$i"
     done
 }
@@ -380,7 +382,28 @@ def deb_list_beautify(d, varname):
 # shall be used outside of this class.
 
 def insert_isar_mounts(d, rootfs, mounts):
+    """
+    In unshare mode, all mounts must be created after unsharing the
+    mount namespace. As needs to happen within the unshared session,
+    we implement it as a code generator. Note, that the random and urandom
+    mounts are needed for DDI images.
+    """
     lines = []
+    to_touch = ['/dev/null', '/dev/random', '/dev/urandom']
+    to_mkdir = ['/dev/pts', '/dev/shm']
+    if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+        lines.append('touch ' + ' '.join(['{}/{}'.format(rootfs, f) for f in to_touch]))
+        lines.append('mkdir -p ' + ' '.join(['{}/{}'.format(rootfs, f) for f in to_mkdir]))
+        lines.append('mount -o bind,private,mode=666 /dev/null {}/dev/null'.format(rootfs))
+        lines.append('mount -t devpts -o noexec,nosuid,uid=5,mode=620,ptmxmode=666 none {}/dev/pts'.format(rootfs))
+        lines.append('( cd {}/dev; ln -sf pts/ptmx . )'.format(rootfs))
+        lines.append('mount -t tmpfs none {}/dev/shm'.format(rootfs))
+        lines.append('mount -o bind /dev/random {}/dev/random'.format(rootfs))
+        lines.append('mount -o bind /dev/urandom {}/dev/urandom'.format(rootfs))
+        lines.append('mount -t proc none {}/proc'.format(rootfs))
+        # we do not unshare the network namespace, so we cannot create a sysfs, hence bind-mount
+        lines.append('mount -o rbind /sys {}/sys'.format(rootfs))
+
     for m in mounts.split():
         host, inner = m.split(':') if ':' in m else (m, m)
         inner_full = os.path.join(rootfs, inner[1:])
@@ -389,7 +412,18 @@ def insert_isar_mounts(d, rootfs, mounts):
     return '\n'.join(lines)
 
 def insert_isar_umounts(d, rootfs, mounts):
+    """
+    In unshare mount we don't unmount the system mounts but just
+    remove the mountpoints.
+    """
     lines = []
+    to_unlink = ['/dev/null', '/dev/random', '/dev/urandom', '/dev/ptmx']
+    to_rmdir = ['/dev/pts', '/dev/shm']
+    if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+        lines.append('rm -f ' + ' '.join(['{}/{}'.format(rootfs, f) for f in to_unlink]))
+        for d in ['{}/{}'.format(rootfs, _d) for _d in to_rmdir]:
+            lines.append('[ -d {} ] && rmdir {}'.format(d, d))
+
     for m in mounts.split():
         host, inner = m.split(':') if ':' in m else (m, m)
         mp = '{}/{}'.format(rootfs, inner)
@@ -397,11 +431,52 @@ def insert_isar_umounts(d, rootfs, mounts):
         lines.append('[ -d {} ] && rmdir --ignore-fail-on-non-empty {}'.format(mp, mp))
     return '\n'.join(lines)
 
+def get_subid_range(idmap, d):
+    import getpass
+    with open(idmap, 'r') as f:
+        entries = f.readlines()
+    for e in entries:
+        user, base, cnt = e.split(':')
+        if user == os.getuid() or user == getpass.getuser():
+            return int(base), int(cnt)
+    bb.error("No sub-id range specified in %s" % idmap)
+
 def run_privileged_cmd(d):
-    cmd = 'sudo -E'
+    """
+    In unshare mode we need to map the rootfs uid/gid range into the
+    subuid/subgid range of the parent namespace. As we usually only
+    get 65534 ids, we cannot map the whole range, as two ids are already
+    used by the calling environment (root and builder user). Hence, map
+    as much as we can but also map the highest id (nobody / nogroup) as
+    these are used within the rootfs. It would be easier to use
+    mmdebstrap --unshare-helper as command (which is also internally used
+    by sbuild), but this only maps linear ranges, hence it cannot map the
+    nobody / nogroup on the default subid range. By that, we have to avoid
+    the nobody / nogroup when building packages in this case.
+    """
+    if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+        nobody_id = 65534
+        uid_base, uid_cnt = get_subid_range('/etc/subuid', d)
+        nobody_subid = uid_base + uid_cnt - 1
+        gid_base, gid_cnt = get_subid_range('/etc/subgid', d)
+        nogroup_subid = gid_base + gid_cnt - 1
+        cmd = 'unshare --mount --pid --uts --ipc --user' \
+              ' --kill-child' \
+              ' --setuid 0 --setgid 0 --fork' \
+              f' --map-users  1:{uid_base+1}:{uid_cnt-2}' \
+              f' --map-groups 1:{gid_base+1}:{gid_cnt-2}'
+        if uid_cnt < nobody_id:
+            cmd += f' --map-users  {nobody_id}:{nobody_subid}:1'
+        if gid_cnt < nobody_id:
+            cmd += f' --map-groups {nobody_id}:{nogroup_subid}:1'
+        cmd += " --map-root-user"
+    else:
+        cmd = 'sudo -E'
     bb.debug(1, "privileged cmd: %s" % cmd)
     return cmd
 
+UNSHARE_SUBUID_BASE  := "${@get_subid_range('/etc/subuid', d)[0] if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else '0'}"
+# store in variable to only compute once and make available to fetcher
 RUN_PRIVILEGED_CMD := "${@run_privileged_cmd(d)}"
 
 run_privileged() {
@@ -415,5 +490,10 @@ run_privileged_heredoc() {
 run_in_chroot() {
     rootfs="$1"
     shift
-    ${RUN_PRIVILEGED_CMD} chroot "$rootfs" "$@"
+
+    rootfs=$rootfs run_privileged_heredoc <<'EORIC' "$@"
+        set -e
+        ${@insert_isar_mounts(d, '$rootfs', '')}
+        chroot "$rootfs" "$@"
+EORIC
 }
diff --git a/meta/classes-recipe/deb-dl-dir.bbclass b/meta/classes-recipe/deb-dl-dir.bbclass
index 04fd6414..0e268f06 100644
--- a/meta/classes-recipe/deb-dl-dir.bbclass
+++ b/meta/classes-recipe/deb-dl-dir.bbclass
@@ -123,8 +123,13 @@ deb_dl_dir_import() {
 
     # let our unprivileged user place downloaded packages in /var/cache/apt/archives/
     run_privileged_heredoc << '    EOSUDO'
-        mkdir -p "${rootfs}"/var/cache/apt/archives/partial/
-        chown -R ${uid}:${gid} "${rootfs}"/var/cache/apt/archives/
+        if [ "${ISAR_CHROOT_MODE}" = "unshare" ]; then
+            mkdir -p "${rootfs}"/var/cache/apt/archives
+            chmod 777 "${rootfs}"/var/cache/apt/archives
+        else
+            mkdir -p "${rootfs}"/var/cache/apt/archives/partial/
+            chown -R ${uid}:${gid} "${rootfs}"/var/cache/apt/archives/
+        fi
     EOSUDO
 
     # nothing to copy if download directory does not exist just yet
diff --git a/meta/classes-recipe/dpkg-base.bbclass b/meta/classes-recipe/dpkg-base.bbclass
index e8721c79..a0d4fd05 100644
--- a/meta/classes-recipe/dpkg-base.bbclass
+++ b/meta/classes-recipe/dpkg-base.bbclass
@@ -168,12 +168,30 @@ dpkg_schroot_create_configs() {
 EOSUDO
 }
 
+dpkg_chroot_prepare() {
+    if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+        dpkg_schroot_create_configs
+    fi
+}
+
+dpkg_chroot_finalize() {
+    if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+        schroot_delete_configs
+    fi
+}
+
+dpkg_prepare_unshare_ccache() {
+    mkdir -p "${CCACHE_DIR}"
+    # sbuild id from https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1110942
+    setfacl -m u:${UNSHARE_SUBUID_BASE}:rwX -m u:${@int(d.getVar('UNSHARE_SUBUID_BASE')) + 999}:rwx "${CCACHE_DIR}"
+}
+
 python do_dpkg_build() {
-    bb.build.exec_func('dpkg_schroot_create_configs', d)
+    bb.build.exec_func('dpkg_chroot_prepare', d)
     try:
         bb.build.exec_func("dpkg_runbuild", d)
     finally:
-        bb.build.exec_func('schroot_delete_configs', d)
+        bb.build.exec_func('dpkg_chroot_finalize', d)
 }
 do_dpkg_build[network] = "${TASK_USE_NETWORK_AND_SUDO}"
 
diff --git a/meta/classes-recipe/dpkg.bbclass b/meta/classes-recipe/dpkg.bbclass
index dcdef487..57fe042b 100644
--- a/meta/classes-recipe/dpkg.bbclass
+++ b/meta/classes-recipe/dpkg.bbclass
@@ -85,7 +85,10 @@ dpkg_runbuild() {
     ext_deb_dir="${ext_root}${deb_dir}"
 
     if [ ${USE_CCACHE} -eq 1 ]; then
-        schroot_configure_ccache
+        ${ISAR_CHROOT_MODE}_configure_ccache
+    fi
+    if [ "${ISAR_CHROOT_MODE}" = "unshare" ]; then
+        sbuild_add_unshare_mounts
     fi
 
     profiles="${@ isar_deb_build_profiles(d)}"
@@ -109,23 +112,27 @@ dpkg_runbuild() {
 
     DSC_FILE=$(find ${WORKDIR} -maxdepth 1 -name "${DEBIAN_SOURCE}_*.dsc" -print)
 
-    sbuild -A -n -c ${SBUILD_CHROOT} --chroot-mode=schroot \
+    sbuild -A -n -c ${SBUILD_CHROOT} \
+        --chroot-mode=${ISAR_CHROOT_MODE} \
         --host=${PACKAGE_ARCH} --build=${BUILD_ARCH} ${profiles} \
         --no-run-lintian --no-run-piuparts --no-run-autopkgtest --resolve-alternatives \
         --bd-uninstallable-explainer=apt \
         --no-apt-update --apt-distupgrade \
         --chroot-setup-commands="echo \"Package: *\nPin: release n=${DEBDISTRONAME}\nPin-Priority: 1000\" > /etc/apt/preferences.d/isar-apt" \
-        --chroot-setup-commands="echo \"APT::Get::allow-downgrades 1;\" > /etc/apt/apt.conf.d/50isar-apt" \
+        --chroot-setup-commands="echo \"APT::Get::allow-downgrades 1;${@'\nAPT::Sandbox::User root;' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}\" > /etc/apt/apt.conf.d/50isar-apt" \
         --chroot-setup-commands="rm -f /var/log/dpkg.log" \
         --chroot-setup-commands="mkdir -p ${deb_dir}" \
         --chroot-setup-commands="find ${ext_deb_dir} -maxdepth 1 -name '*.deb' -exec ln -t ${deb_dir}/ -sf {} +" \
         --chroot-setup-commands="apt-get update -o Dir::Etc::SourceList=\"sources.list.d/isar-apt.list\" -o Dir::Etc::SourceParts=\"-\" -o APT::Get::List-Cleanup=\"0\"" \
         --finished-build-commands="rm -f ${deb_dir}/sbuild-build-depends-*-dummy_*.deb" \
         --finished-build-commands="find ${deb_dir} -maxdepth 1 -type f -name '*.deb' -print -exec cp ${CP_FLAGS} -t ${ext_deb_dir}/ {} +" \
-        --finished-build-commands="cp /var/log/dpkg.log ${ext_root}/dpkg_partial.log" \
+        ${@ '--finished-build-commands="cp /var/log/dpkg.log $ext_root/dpkg_partial.log"' if d.getVar('ISAR_CHROOT_MODE') == 'schroot' else '' } \
         --build-path="" --build-dir=${WORKDIR} --dist="${DEBDISTRONAME}" ${DSC_FILE}
 
-    sbuild_dpkg_log_export "${WORKDIR}/rootfs/dpkg_partial.log"
+    # TODO: port to unshare backend
+    if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+        sbuild_dpkg_log_export "${WORKDIR}/rootfs/dpkg_partial.log"
+    fi
     deb_dl_dir_export "${WORKDIR}/rootfs" "${distro}"
 
     # Cleanup apt artifacts
diff --git a/meta/classes-recipe/image-locales-extension.bbclass b/meta/classes-recipe/image-locales-extension.bbclass
index 029caec7..9bb43a8d 100644
--- a/meta/classes-recipe/image-locales-extension.bbclass
+++ b/meta/classes-recipe/image-locales-extension.bbclass
@@ -29,8 +29,12 @@ ROOTFS_INSTALL_COMMAND_BEFORE_EXPORT += "image_install_localepurge_download"
 image_install_localepurge_download[weight] = "40"
 image_install_localepurge_download[network] = "${TASK_USE_NETWORK_AND_SUDO}"
 image_install_localepurge_download() {
-    run_in_chroot '${ROOTFSDIR}' \
+    run_privileged_heredoc <<'EOF'
+    set -e
+    ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS') if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else '')}
+    chroot ${ROOTFSDIR} \
         /usr/bin/apt-get ${ROOTFS_APT_ARGS} -oDebug::NoLocking=1 --download-only localepurge
+EOF
 }
 
 ROOTFS_INSTALL_COMMAND += "image_install_localepurge_install"
@@ -62,6 +66,9 @@ __EOF__
     # Install configuration into image:
     run_privileged_heredoc <<'EOSUDO'
         set -e
+
+        ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), '')}
+
         localepurge_state='i'
         if chroot '${ROOTFSDIR}' dpkg -s localepurge 2>/dev/null >&2
         then
diff --git a/meta/classes-recipe/image-tools-extension.bbclass b/meta/classes-recipe/image-tools-extension.bbclass
index 766f386d..cc046fdb 100644
--- a/meta/classes-recipe/image-tools-extension.bbclass
+++ b/meta/classes-recipe/image-tools-extension.bbclass
@@ -16,7 +16,14 @@ do_image_tools[depends] += " \
 SCHROOT_MOUNTS = "${WORKDIR}:${PP_WORK} ${IMAGE_ROOTFS}:${PP_ROOTFS} ${DEPLOY_DIR_IMAGE}:${PP_DEPLOY}"
 SCHROOT_MOUNTS += "${REPO_ISAR_DIR}/${DISTRO}:/isar-apt"
 
+# only used on unshare
+ROOTFS_IMAGETOOLS ?= "${WORKDIR}/rootfs-imgtools-${BB_CURRENTTASK}"
+
 imager_run() {
+    imager_run_${ISAR_CHROOT_MODE} "$@"
+}
+
+imager_run_schroot() {
     local_install="${@(d.getVar("INSTALL_%s" % d.getVar("BB_CURRENTTASK")) or '').strip()}"
     local_bom="${@(d.getVar("BOM_%s" % d.getVar("BB_CURRENTTASK")) or '').strip()}"
 
@@ -103,3 +110,80 @@ generate_imager_sbom() {
             --timestamp $TIMESTAMP ${SBOM_DEBSBOM_EXTRA_ARGS} \
     < ${WORKDIR}/imager.manifest
 }
+
+imager_run_unshare() {
+    exec 3<&0
+
+    # ignore everything before '--'. If the remaining list is empty,
+    # assume a here document is passed via stdin
+    while [ "$#" -gt 0 ]; do
+        case "$1" in
+        --) shift 1; break ;;
+        *) shift 1 ;;
+        esac
+    done
+
+    if [ "$#" -eq 0 ]; then
+        set -- "$@" '/bin/bash' '-s'
+    fi
+
+    local_install="${@(d.getVar("INSTALL_%s" % d.getVar("BB_CURRENTTASK")) or '').strip()}"
+
+    run_privileged_heredoc <<'EOF'
+    set -e
+    mkdir -p ${ROOTFS_IMAGETOOLS}
+    tar -xf "${SBUILD_CHROOT}" -C "${ROOTFS_IMAGETOOLS}"
+    mkdir -p ${ROOTFS_IMAGETOOLS}/isar-apt
+    cp -rL /etc/resolv.conf "${ROOTFS_IMAGETOOLS}/etc"
+EOF
+
+    # setting up error handler
+    imager_cleanup() {
+        run_privileged rm -rf ${ROOTFS_IMAGETOOLS}
+    }
+    trap 'exit 1' INT HUP QUIT TERM ALRM USR1
+    trap 'imager_cleanup' EXIT
+
+    if [ -n "${local_install}" ]; then
+        echo "Installing imager deps: ${local_install}"
+
+        distro="${BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
+        if [ ${ISAR_CROSS_COMPILE} -eq 1 ]; then
+            distro="${HOST_BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
+        fi
+
+        E="${@ isar_export_proxies(d)}"
+        deb_dl_dir_import ${ROOTFS_IMAGETOOLS} ${distro}
+        ${SCRIPTSDIR}/lockrun.py -r -f "${REPO_ISAR_DIR}/isar.lock" -s <<'EOAPT'
+        local_install=$local_install ${@run_privileged_cmd(d)} /bin/bash -s <<'EOF'
+            set -e
+            ${@insert_isar_mounts(d, d.getVar('ROOTFS_IMAGETOOLS'), d.getVar('SCHROOT_MOUNTS'))}
+            chroot ${ROOTFS_IMAGETOOLS} apt-get update \
+                -o Dir::Etc::SourceList='sources.list.d/isar-apt.list' \
+                -o Dir::Etc::SourceParts='-' \
+                -o APT::Get::List-Cleanup='0'
+            chroot ${ROOTFS_IMAGETOOLS} apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y \
+                --allow-unauthenticated --allow-downgrades --download-only install \
+                $local_install
+EOF
+EOAPT
+
+        deb_dl_dir_export ${ROOTFS_IMAGETOOLS} ${distro}
+        local_install=$local_install run_privileged_heredoc <<'EOF'
+            set -e
+            ${@insert_isar_mounts(d, d.getVar('ROOTFS_IMAGETOOLS'), d.getVar('SCHROOT_MOUNTS'))}
+            chroot ${ROOTFS_IMAGETOOLS} apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y \
+                --allow-unauthenticated --allow-downgrades install \
+                $local_install
+EOF
+    fi
+
+    run_privileged_heredoc <<'EOF' "$@"
+        set -e
+        mkdir -p ${ROOTFS_IMAGETOOLS}/${SCRIPTSDIR}
+        ${@insert_isar_mounts(d, d.getVar('ROOTFS_IMAGETOOLS'), d.getVar('SCHROOT_MOUNTS'))}
+        chroot ${ROOTFS_IMAGETOOLS} "$@" <&3
+EOF
+
+    run_privileged rm -rf ${ROOTFS_IMAGETOOLS}
+}
diff --git a/meta/classes-recipe/image.bbclass b/meta/classes-recipe/image.bbclass
index 9fcdda48..0fa15a87 100644
--- a/meta/classes-recipe/image.bbclass
+++ b/meta/classes-recipe/image.bbclass
@@ -189,6 +189,7 @@ SUDO_CHROOT = "imager_run -d ${PP_ROOTFS} -u root --"
 python() {
     image_types = (d.getVar('IMAGE_FSTYPES') or '').split()
     conversions = set(d.getVar('IMAGE_CONVERSIONS').split())
+    chroot_mode = d.getVar('ISAR_CHROOT_MODE')
 
     basetypes = {}
     typedeps = {}
@@ -264,7 +265,8 @@ python() {
         if image_cmd:
             localdata.setVar('type', bt)
             cmds.append(localdata.expand(image_cmd))
-            cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}'))
+            if chroot_mode == 'schroot':
+                cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}'))
         else:
             bb.fatal("No IMAGE_CMD for %s" % bt)
         vardeps.add('IMAGE_CMD:' + bt_clean)
@@ -294,7 +296,8 @@ python() {
                     cmd = '\t' + localdata.getVar('CONVERSION_CMD:' + c)
                     if cmd not in cmds:
                         cmds.append(cmd)
-                        cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}.%s' % c))
+                        if chroot_mode == 'schroot':
+                            cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}.%s' % c))
                     vardeps.add('CONVERSION_CMD:' + c)
                     for dep in (localdata.getVar('CONVERSION_DEPS:' + c) or '').split():
                         conversion_install.add(dep)
diff --git a/meta/classes-recipe/imagetypes_container.bbclass b/meta/classes-recipe/imagetypes_container.bbclass
index fb1d0cdf..a68438e9 100644
--- a/meta/classes-recipe/imagetypes_container.bbclass
+++ b/meta/classes-recipe/imagetypes_container.bbclass
@@ -68,7 +68,9 @@ do_containerize() {
     run_privileged rm -rf "${oci_img_dir}_unpacked"
 
     # no root needed anymore
-    run_privileged chown --recursive $(id -u):$(id -g) "${oci_img_dir}"
+    if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+        run_privileged chown --recursive $(id -u):$(id -g) "${oci_img_dir}"
+    fi
 }
 
 convert_container() {
diff --git a/meta/classes-recipe/imagetypes_wic.bbclass b/meta/classes-recipe/imagetypes_wic.bbclass
index 8b048dc7..3e261622 100644
--- a/meta/classes-recipe/imagetypes_wic.bbclass
+++ b/meta/classes-recipe/imagetypes_wic.bbclass
@@ -193,8 +193,10 @@ generate_wic_image() {
         fi
 EOIMAGER
 
-    run_privileged chown -R $(stat -c "%U" ${LAYERDIR_core}) ${LAYERDIR_core} ${LAYERDIR_isar} ${SCRIPTSDIR} || true
-    run_privileged chown -R $(id -u):$(id -g) "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}.wic"*
+    if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+        run_privileged chown -R $(stat -c "%U" ${LAYERDIR_core}) ${LAYERDIR_core} ${LAYERDIR_isar} ${SCRIPTSDIR} || true
+        run_privileged chown -R $(id -u):$(id -g) "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}.wic"*
+    fi
     rm -rf ${IMAGE_ROOTFS}/../pseudo
 
     cat ${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}.manifest \
diff --git a/meta/classes-recipe/rootfs.bbclass b/meta/classes-recipe/rootfs.bbclass
index 7352a87c..f31964db 100644
--- a/meta/classes-recipe/rootfs.bbclass
+++ b/meta/classes-recipe/rootfs.bbclass
@@ -145,7 +145,12 @@ rootfs_cmd() {
 }
 
 rootfs_do_mounts[weight] = "3"
-rootfs_do_mounts() {
+python rootfs_do_mounts() {
+    if d.getVar('ISAR_CHROOT_MODE') == 'schroot':
+        bb.build.exec_func('rootfs_do_mounts_priv', d)
+}
+
+rootfs_do_mounts_priv() {
     run_privileged_heredoc <<'EOSUDO'
         set -e
         mountpoint -q '${ROOTFSDIR}/dev' || \
@@ -168,7 +173,13 @@ rootfs_do_mounts() {
 EOSUDO
 }
 
-rootfs_do_umounts() {
+python rootfs_do_umounts() {
+    # unconditionally run the unmount code as this ignores missing
+    # mountpoints but also does the cleanup of the directories
+    bb.build.exec_func('rootfs_do_umounts_priv', d)
+}
+
+rootfs_do_umounts_priv() {
     run_privileged_heredoc <<'EOSUDO'
         set -e
 
@@ -215,7 +226,11 @@ ROOTFS_EXTRA_IMPORTED := "${@rootfs_extra_import(d)}"
 
 rootfs_prepare[weight] = "25"
 rootfs_prepare(){
-    run_privileged tar -xf "${BOOTSTRAP_SRC}" -C "${ROOTFSDIR}" --exclude="./dev/console"
+    rm -rf ${ROOTFSDIR}
+    run_privileged_heredoc << 'EOF'
+        mkdir -p ${ROOTFSDIR}
+        tar -xf "${BOOTSTRAP_SRC}" -C "${ROOTFSDIR}" --exclude="./dev/console"
+EOF
 
     # setup chroot
     run_privileged "${ROOTFSDIR}/chroot-setup.sh" "setup" "${ROOTFSDIR}"
@@ -285,10 +300,14 @@ rootfs_install_pkgs_update[weight] = "5"
 rootfs_install_pkgs_update[isar-apt-lock] = "acquire-before"
 rootfs_install_pkgs_update[network] = "${TASK_USE_NETWORK_AND_SUDO}"
 rootfs_install_pkgs_update() {
-    run_in_chroot '${ROOTFSDIR}' /usr/bin/apt-get update \
-        -o Dir::Etc::SourceList="sources.list.d/isar-apt.list" \
-        -o Dir::Etc::SourceParts="-" \
-        -o APT::Get::List-Cleanup="0"
+    run_privileged_heredoc <<'EOF'
+        set -e
+        ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+        chroot '${ROOTFSDIR}' /usr/bin/apt-get update \
+            -o Dir::Etc::SourceList="sources.list.d/isar-apt.list" \
+            -o Dir::Etc::SourceParts="-" \
+            -o APT::Get::List-Cleanup="0"
+EOF
 }
 
 ROOTFS_INSTALL_COMMAND += "rootfs_install_resolvconf"
@@ -316,9 +335,12 @@ rootfs_install_pkgs_download[isar-apt-lock] = "release-after"
 rootfs_install_pkgs_download[network] = "${TASK_USE_NETWORK}"
 rootfs_install_pkgs_download() {
     # download packages using apt in a non-privileged namespace
-    rootfs_cmd --bind "${ROOTFSDIR}/var/cache/apt/archives" /var/cache/apt/archives \
-               ${ROOTFSDIR} \
-               -- /usr/bin/apt-get ${ROOTFS_APT_ARGS} -oDebug::NoLocking=1 --download-only ${ROOTFS_PACKAGES}
+    run_privileged_heredoc <<'EOF'
+    set -e
+    ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+    chroot ${ROOTFSDIR} \
+        /usr/bin/apt-get ${ROOTFS_APT_ARGS} -oDebug::NoLocking=1 --download-only ${ROOTFS_PACKAGES}
+EOF
 }
 
 ROOTFS_INSTALL_COMMAND_BEFORE_EXPORT ??= ""
@@ -345,8 +367,12 @@ rootfs_install_pkgs_install[weight] = "8000"
 rootfs_install_pkgs_install[progress] = "custom:rootfs_progress.PkgsInstallProgressHandler"
 rootfs_install_pkgs_install[network] = "${TASK_USE_SUDO}"
 rootfs_install_pkgs_install() {
-    run_in_chroot "${ROOTFSDIR}" \
+    run_privileged_heredoc <<'EOF'
+    set -e
+    ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+    chroot "${ROOTFSDIR}" \
         /usr/bin/apt-get ${ROOTFS_APT_ARGS} ${ROOTFS_PACKAGES}
+EOF
 }
 
 ROOTFS_INSTALL_COMMAND += "rootfs_restore_initrd_tooling"
@@ -653,8 +679,10 @@ rootfs_install_sstate_finalize() {
     # - after building the rootfs, the tar won't be there, but we also don't need to unpack
     # - after restoring from cache, there will be a tar which we unpack and then delete
     if [ -f rootfs.tar ]; then
+        run_privileged_heredoc <<'EOF'
         mkdir -p ${ROOTFSDIR}
-        run_privileged tar -C ${ROOTFSDIR} -xp ${SSTATE_TAR_ATTR_FLAGS} < rootfs.tar
+        tar -C ${ROOTFSDIR} -xp ${SSTATE_TAR_ATTR_FLAGS} -f rootfs.tar
+EOF
         rm rootfs.tar
     fi
 }
diff --git a/meta/classes-recipe/sbuild.bbclass b/meta/classes-recipe/sbuild.bbclass
index d9ccce7f..8ca66138 100644
--- a/meta/classes-recipe/sbuild.bbclass
+++ b/meta/classes-recipe/sbuild.bbclass
@@ -7,7 +7,8 @@ SCHROOT_MOUNTS ?= ""
 
 inherit crossvars
 
-SBUILD_CHROOT ?= "${DEBDISTRONAME}-${SCHROOT_USER}-${ISAR_BUILD_UUID}-${@os.getpid()}"
+SBUILD_CHROOT:unshare ?= "${SCHROOT_DIR}.tar.zst"
+SBUILD_CHROOT:schroot ?= "${DEBDISTRONAME}-${SCHROOT_USER}-${ISAR_BUILD_UUID}-${@os.getpid()}"
 
 SBUILD_CONF_DIR ?= "${SCHROOT_CONF}/${SBUILD_CHROOT}"
 SCHROOT_CONF_FILE ?= "${SCHROOT_CONF}/chroot.d/${SBUILD_CHROOT}"
@@ -144,6 +145,13 @@ END
 EOSUDO
 }
 
+unshare_configure_ccache() {
+    # ccache must be below /build for file permissions to work properly
+    cat <<'EOF' >> ${SBUILD_CONFIG}
+$path = "/usr/lib/ccache:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games";
+EOF
+}
+
 sbuild_dpkg_log_export() {
     export dpkg_partial_log="${1}"
 
@@ -152,3 +160,17 @@ sbuild_dpkg_log_export() {
     cat ${dpkg_partial_log} >> ${SCHROOT_DIR}/tmp/dpkg_common.log
     )  9>"${SCHROOT_DIR}/tmp/dpkg_common.log.lock"
 }
+
+# additional mounts managed by sbuild
+sbuild_add_unshare_mounts() {
+    dpkg_prepare_unshare_ccache
+
+    cat <<'EOF' >> ${SBUILD_CONFIG}
+$unshare_bind_mounts = [
+    { directory => '${WORKDIR}/rootfs', mountpoint => '${PP}/rootfs' },
+    { directory => '${WORKDIR}/isar-apt/${DISTRO}-${DISTRO_ARCH}/apt/${DISTRO}', mountpoint => '/isar-apt' },
+    { directory => '${REPO_BASE_DIR}', mountpoint => '/base-apt' },
+    { directory => "${CCACHE_DIR}", mountpoint => "/ccache" }
+];
+EOF
+}
diff --git a/meta/classes-recipe/sdk.bbclass b/meta/classes-recipe/sdk.bbclass
index 16165792..7a8d5ff4 100644
--- a/meta/classes-recipe/sdk.bbclass
+++ b/meta/classes-recipe/sdk.bbclass
@@ -74,13 +74,17 @@ rootfs_configure_isar_apt_dir() {
 
 ROOTFS_POSTPROCESS_COMMAND:prepend:class-sdk = "sdkchroot_configscript "
 sdkchroot_configscript () {
-    run_in_chroot ${ROOTFSDIR} /configscript.sh ${DISTRO_ARCH}
+    run_privileged_heredoc <<'EOF'
+        set -e
+        ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+        cp -rL /etc/resolv.conf '${ROOTFSDIR}/etc'
+        chroot ${ROOTFSDIR} /configscript.sh ${DISTRO_ARCH}
+EOF
 }
 
 ROOTFS_POSTPROCESS_COMMAND:append:class-sdk = " sdkchroot_finalize"
 sdkchroot_finalize() {
-
-    rootfs_do_umounts
+    rootfs_do_umounts_priv
 
     # Remove setup scripts
     run_privileged rm -f ${ROOTFSDIR}/chroot-setup.sh ${ROOTFSDIR}/configscript.sh
diff --git a/meta/conf/bitbake.conf b/meta/conf/bitbake.conf
index 5f339d40..4d3fd62e 100644
--- a/meta/conf/bitbake.conf
+++ b/meta/conf/bitbake.conf
@@ -73,7 +73,7 @@ KERNEL_FILE:arm64 ?= "vmlinux"
 
 MACHINEOVERRIDES ?= "${MACHINE}"
 DISTROOVERRIDES ?= "${DISTRO}"
-OVERRIDES = "${PACKAGE_ARCH}:${MACHINEOVERRIDES}:${DISTROOVERRIDES}:${BASE_DISTRO_CODENAME}:forcevariable"
+OVERRIDES = "${PACKAGE_ARCH}:${MACHINEOVERRIDES}:${DISTROOVERRIDES}:${BASE_DISTRO_CODENAME}:${ISAR_CHROOT_MODE}:forcevariable"
 FILESOVERRIDES = "${PACKAGE_ARCH}:${MACHINE}"
 
 # Setting default QEMU_ARCH variables for different DISTRO_ARCH:
@@ -152,6 +152,10 @@ ISAR_APT_RETRIES ??= "${@'10' if bb.utils.to_boolean(d.getVar('ISAR_USE_APT_SNAP
 ISAR_APT_DELAY_MAX ??= "${@'600' if bb.utils.to_boolean(d.getVar('ISAR_USE_APT_SNAPSHOT')) else ''}"
 ISAR_APT_SNAPSHOT_TIMESTAMP ??= "${SOURCE_DATE_EPOCH}"
 
+# Rootless build execution
+ISAR_ROOTLESS ??= "0"
+ISAR_CHROOT_MODE ??= "${@'unshare' if bb.utils.to_boolean(d.getVar('ISAR_ROOTLESS')) else 'schroot'}"
+
 # Default parallelism and resource usage for xz
 XZ_MEMLIMIT ?= "50%"
 XZ_THREADS ?= "${@oe.utils.cpu_count(at_least=2)}"
@@ -207,6 +211,7 @@ CCACHE_DEBUG ?= "0"
 # Variables for tasks marking
 # Long term TODO: get rid of sudo marked tasks
 TASK_USE_NETWORK = "1"
+# nested namespacing requires this as well
 TASK_USE_SUDO = "1"
 TASK_USE_NETWORK_AND_SUDO = "1"
 
diff --git a/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc b/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc
index cf6c355c..4d102ed6 100644
--- a/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc
+++ b/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc
@@ -161,6 +161,8 @@ do_bootstrap() {
             line="[trusted=yes] ${line}"
         fi
         echo "deb-src ${line}" >>  "${WORKDIR}/sources.list.d/base-apt.list"
+        echo > ${WORKDIR}/mmtmpdir
+        chmod 666 ${WORKDIR}/mmtmpdir
 
         # no need to sync /var/cache/apt/archives if base-apt used
         syncin='echo skip sync-in'
@@ -177,12 +179,14 @@ do_bootstrap() {
                          mkdir -p \$1/base-apt && \
                          mount -o bind,private '${REPO_BASE_DIR}' \$1/base-apt && \
                          chroot \$1 apt-get update -y \
-                                -o APT::Update::Error-Mode=any && \
+                                -o APT::Update::Error-Mode=any \
+                                ${@'-o APT::Sandbox::User=root' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''} && \
                          chroot \$1 apt-get install -y dpkg && \
                          umount \$1/base-apt && \
-                         umount \$1/$base_apt_tmp && rm ${WORKDIR}/mmtmpdir && \
-                         umount $base_apt_tmp && rm -rf --one-file-system $base_apt_tmp"
+                         umount \$1/$base_apt_tmp && \
+                         umount $base_apt_tmp && rmdir \$1/$base_apt_tmp"
     else
+        # prepare dl_dir for access from both sides (local and rootfs)
         deb_dl_dir_import "${WORKDIR}/dl_dir" "${BOOTSTRAP_BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
 
         bootstrap_list="${WORKDIR}/sources.list.d/bootstrap.list"
@@ -202,6 +206,7 @@ do_bootstrap() {
                                  -o Dir::State="$1/var/lib/apt" \
                                  -o Dir::Etc="$1/etc/apt" \
                                  -o Dir::Cache="$1/var/cache/apt" \
+                                 ${@'-o APT::Sandbox::User=root' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''} \
                                  -o Apt::Architecture="${BOOTSTRAP_DISTRO_ARCH}" \
                                  ${@get_apt_opts(d, '-o')}'
         extra_essential="$extra_essential && $syncout"
@@ -225,7 +230,8 @@ do_bootstrap() {
     mkdir -p ${DEBDIR}
     touch ${DEB_DL_LOCK}
 
-    run_privileged TMPDIR="${BOOTSTRAP_TMPDIR}" mmdebstrap $bootstrap_args \
+    ${@'' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else 'run_privileged'} \
+    TMPDIR="${BOOTSTRAP_TMPDIR}" mmdebstrap $bootstrap_args \
                    $arch_param \
                    --mode=unshare \
                    ${MMHOOKS} \
@@ -244,6 +250,7 @@ do_bootstrap() {
                    --customize-hook='sed -i "/en_US.UTF-8 UTF-8/s/^#//g" "$1/etc/locale.gen"' \
                    --customize-hook='chroot "$1" /usr/sbin/locale-gen' \
                    --customize-hook='chroot "$1" /usr/bin/apt-get -y clean' \
+                   ${@'--skip=output/dev' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''} \
                    --skip=cleanup/apt \
                    --skip=download/empty \
                    ${MMOPTS} \
@@ -258,7 +265,8 @@ do_bootstrap() {
 
     if [ "${ISAR_USE_CACHED_BASE_REPO}" != "1" ]; then
         deb_dl_dir_export "${WORKDIR}/dl_dir" "${BOOTSTRAP_BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
-        run_privileged rm -rf --one-file-system "${WORKDIR}/dl_dir"
+        run_privileged find ${WORKDIR}/dl_dir -maxdepth 1 -mindepth 1 -exec rm -rf --one-file-system "{}" \;
+        rmdir ${WORKDIR}/dl_dir
     fi
 }
 addtask bootstrap before do_build after do_generate_keyrings
diff --git a/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc b/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc
index aa62b324..054d7fc2 100644
--- a/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc
+++ b/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc
@@ -66,8 +66,28 @@ ROOTFS_POSTPROCESS_COMMAND:remove = "rootfs_cleanup_base_apt"
 
 DEPLOY_SCHROOT = "${@d.getVar('SCHROOT_' + d.getVar('SBUILD_VARIANT').upper() + '_DIR')}${SBUILD_SCHROOT_SUFFIX}"
 
-do_sbuildchroot_deploy[dirs] = "${DEPLOY_DIR}/schroot-${SBUILD_VARIANT}"
-do_sbuildchroot_deploy() {
+sbuildchroot_deploy_tree() {
     ln -Tfsr "${ROOTFSDIR}" "${DEPLOY_SCHROOT}"
 }
+sbuildchroot_deploy_tar() {
+    lopts="--one-file-system --exclude=var/cache/apt/archives --exclude=isar-apt"
+    # we cannot use pzstd, as this results in a different magic
+    # (zstd skippable frame) which is not detected by sbuild
+    # https://salsa.debian.org/debian/sbuild/-/blob/d975d388a98627a0d7d112791e441c27a6d529df/lib/Sbuild/ChrootUnshare.pm#L608
+    ZSTD="zstd -${SSTATE_ZSTD_CLEVEL} -T${ZSTD_THREADS}"
+    run_privileged \
+        tar -C ${ROOTFSDIR} -cpS $lopts ${ROOTFS_TAR_ATTR_FLAGS} . \
+            | $ZSTD > ${DEPLOY_SCHROOT}.tar.zst
+    # cleanup extracted rootfs
+    run_privileged rm -rf ${ROOTFSDIR}
+}
+
+do_sbuildchroot_deploy[network] = "${TASK_USE_SUDO}"
+do_sbuildchroot_deploy[dirs] += "${DEPLOY_DIR}/schroot-${SBUILD_VARIANT}"
+python do_sbuildchroot_deploy() {
+    if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+        bb.build.exec_func('sbuildchroot_deploy_tar', d)
+    else:
+        bb.build.exec_func('sbuildchroot_deploy_tree', d)
+}
 addtask sbuildchroot_deploy before do_build after do_rootfs
