fix(oci): the access rule covers the container's root, and a shared volume is writable by its group

This commit is contained in:
MacRimi
2026-10-08 22:57:37 +02:00
parent 5604094907
commit 66c621fe7a
12 changed files with 54 additions and 20 deletions
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Access Bridge für Nextcloud", "Access bridge for Nextcloud": "Access Bridge für Nextcloud",
"Access bridge for Paperless": "Access Bridge für Paperless", "Access bridge for Paperless": "Access Bridge für Paperless",
"Access bridge for Tandoor": "Access Bridge für Tandoor", "Access bridge for Tandoor": "Access Bridge für Tandoor",
"Access could not be granted; the application cannot write to:": "Der Zugriff konnte nicht gewährt werden;Die Anwendung kann nicht schreiben an:", "Access could not be granted; the application cannot write to:": "Der Zugriff konnte nicht gewährt werden; die Anwendung kann nicht schreiben an:",
"Access granted to the application on:": "Zugriff auf die Anwendung gewährt am:", "Access granted to the application on:": "Zugriff auf die Anwendung gewährt am:",
"Access profile:": "Zugangsprofil:", "Access profile:": "Zugangsprofil:",
"Access token of the Jupyter Lab web interface": "Access-Token des Jupyter Lab Webinterfaces", "Access token of the Jupyter Lab web interface": "Access-Token des Jupyter Lab Webinterfaces",
+1
View File
@@ -767,6 +767,7 @@
"Changing between a privileged and an unprivileged container, or the host monitor mode, requires installing the application again": "Cambiar entre contenedor privilegiado y sin privilegios, o el modo de monitor del host, requiere instalar la aplicación de nuevo", "Changing between a privileged and an unprivileged container, or the host monitor mode, requires installing the application again": "Cambiar entre contenedor privilegiado y sin privilegios, o el modo de monitor del host, requiere instalar la aplicación de nuevo",
"Changing the machine type on an existing installed VM is not safe: it changes the chipset and PCI slot layout, which typically prevents the guest OS from booting.": "Cambiar el tipo de máquina de una VM ya instalada no es seguro: cambia el chipset y la disposición de las ranuras PCI, lo que normalmente impide que arranque el sistema operativo de la VM.", "Changing the machine type on an existing installed VM is not safe: it changes the chipset and PCI slot layout, which typically prevents the guest OS from booting.": "Cambiar el tipo de máquina de una VM ya instalada no es seguro: cambia el chipset y la disposición de las ranuras PCI, lo que normalmente impide que arranque el sistema operativo de la VM.",
"Changing the rootfs or its storage requires a separate migration": "Cambiar los rootfs o su almacenamiento requiere una migración separada", "Changing the rootfs or its storage requires a separate migration": "Cambiar los rootfs o su almacenamiento requiere una migración separada",
"Changing the shared identity (1000:1000) in Settings of the panel means giving the new one access on the host again: the volumes and the access rules are prepared for 1000:1000.": "Cambiar la identidad compartida (1000:1000) en Ajustes del panel obliga a dar acceso de nuevo a la nueva en el host: los volúmenes y las reglas de acceso se preparan para 1000:1000.",
"Changing the storage or size of a disk requires a migration; empty disks are not created": "Cambiar el almacenamiento o el tamaño de un disco requiere una migración; los discos vacíos no se crean", "Changing the storage or size of a disk requires a migration; empty disks are not created": "Cambiar el almacenamiento o el tamaño de un disco requiere una migración; los discos vacíos no se crean",
"Check": "Controlar", "Check": "Controlar",
"Check BIOS/UEFI in Hardware > BIOS — must match what the original VM used": "Verifique BIOS/UEFI en Hardware > BIOS: debe coincidir con lo que usó la VM original", "Check BIOS/UEFI in Hardware > BIOS — must match what the original VM used": "Verifique BIOS/UEFI en Hardware > BIOS: debe coincidir con lo que usó la VM original",
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Pont d'accès pour Nextcloud", "Access bridge for Nextcloud": "Pont d'accès pour Nextcloud",
"Access bridge for Paperless": "Pont d'accès pour sans papier", "Access bridge for Paperless": "Pont d'accès pour sans papier",
"Access bridge for Tandoor": "Pont d'accès pour Tandoor", "Access bridge for Tandoor": "Pont d'accès pour Tandoor",
"Access could not be granted; the application cannot write to:": "L'accès n'a pas pu être accordé ;l'application ne peut pas écrire dans :", "Access could not be granted; the application cannot write to:": "L'accès n'a pas pu être accordé ; l'application ne peut pas écrire dans :",
"Access granted to the application on:": "Accès accordé à l’application sur :", "Access granted to the application on:": "Accès accordé à l’application sur :",
"Access profile:": "Profil d'accès :", "Access profile:": "Profil d'accès :",
"Access token of the Jupyter Lab web interface": "Jeton d'accès de l'interface web Jupyter Lab", "Access token of the Jupyter Lab web interface": "Jeton d'accès de l'interface web Jupyter Lab",
+2 -2
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Bridge di accesso per Nextcloud", "Access bridge for Nextcloud": "Bridge di accesso per Nextcloud",
"Access bridge for Paperless": "Bridge di accesso per Paperless", "Access bridge for Paperless": "Bridge di accesso per Paperless",
"Access bridge for Tandoor": "Bridge di accesso per Tandoor", "Access bridge for Tandoor": "Bridge di accesso per Tandoor",
"Access could not be granted; the application cannot write to:": "impossibile concedere l'accesso;l'applicazione non può scrivere a:", "Access could not be granted; the application cannot write to:": "Impossibile concedere l'accesso; l'applicazione non può scrivere a:",
"Access granted to the application on:": "Accesso concesso all'applicazione su:", "Access granted to the application on:": "Accesso concesso all'applicazione su:",
"Access profile:": "Profilo di accesso:", "Access profile:": "Profilo di accesso:",
"Access token of the Jupyter Lab web interface": "Token di accesso all'interfaccia web di Jupyter Lab", "Access token of the Jupyter Lab web interface": "Token di accesso all'interfaccia web di Jupyter Lab",
@@ -6122,7 +6122,7 @@
"The Valkey volume needs at least 1 GB": "Il volume Valkey richiede almeno 1 GB", "The Valkey volume needs at least 1 GB": "Il volume Valkey richiede almeno 1 GB",
"The acceleration evidence differs from the verified inventory": "I dati sull'accelerazione sono diversi dall'inventario verificato", "The acceleration evidence differs from the verified inventory": "I dati sull'accelerazione sono diversi dall'inventario verificato",
"The acceleration profile does not support this architecture:": "Il profilo di accelerazione non supporta questa architettura:", "The acceleration profile does not support this architecture:": "Il profilo di accelerazione non supporta questa architettura:",
"The access rule added to this host directory is kept:": "la regola di accesso aggiunta a questa directory host viene mantenuta:", "The access rule added to this host directory is kept:": "La regola di accesso aggiunta a questa directory host viene mantenuta:",
"The active kernel driver is not vfio-pci, but the entry in": "Il driver del kernel attivo non è vfio-pci, ma la voce in", "The active kernel driver is not vfio-pci, but the entry in": "Il driver del kernel attivo non è vfio-pci, ma la voce in",
"The active kernel driver is not vfio-pci, but the entry will rebind the GPU to vfio-pci on the next reboot, breaking the LXC passthrough about to be configured.": "Il driver del kernel attivo non è vfio-pci, ma la voce ricollegherà la GPU a vfio-pci al prossimo riavvio, interrompendo il passthrough LXC che sta per essere configurato.", "The active kernel driver is not vfio-pci, but the entry will rebind the GPU to vfio-pci on the next reboot, breaking the LXC passthrough about to be configured.": "Il driver del kernel attivo non è vfio-pci, ma la voce ricollegherà la GPU a vfio-pci al prossimo riavvio, interrompendo il passthrough LXC che sta per essere configurato.",
"The active kernel driver is not vfio-pci, but the entry will rebind the GPU to vfio-pci on the next reboot.": "Il driver del kernel attivo non è vfio-pci, ma la voce ricollegherà la GPU a vfio-pci al prossimo riavvio.", "The active kernel driver is not vfio-pci, but the entry will rebind the GPU to vfio-pci on the next reboot.": "Il driver del kernel attivo non è vfio-pci, ma la voce ricollegherà la GPU a vfio-pci al prossimo riavvio.",
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Ponte de acesso para Nextcloud", "Access bridge for Nextcloud": "Ponte de acesso para Nextcloud",
"Access bridge for Paperless": "Ponte de acesso para Paperless", "Access bridge for Paperless": "Ponte de acesso para Paperless",
"Access bridge for Tandoor": "Ponte de acesso para Tandoor", "Access bridge for Tandoor": "Ponte de acesso para Tandoor",
"Access could not be granted; the application cannot write to:": "O acesso não pôde ser concedido;o aplicativo não pode gravar em:", "Access could not be granted; the application cannot write to:": "O acesso não pôde ser concedido; o aplicativo não pode gravar em:",
"Access granted to the application on:": "Acesso concedido ao aplicativo em:", "Access granted to the application on:": "Acesso concedido ao aplicativo em:",
"Access profile:": "Perfil de acesso:", "Access profile:": "Perfil de acesso:",
"Access token of the Jupyter Lab web interface": "Token de acesso da interface web Jupyter Lab", "Access token of the Jupyter Lab web interface": "Token de acesso da interface web Jupyter Lab",
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Prístupový mostík pre Nextcloud", "Access bridge for Nextcloud": "Prístupový mostík pre Nextcloud",
"Access bridge for Paperless": "Prístupový mostík pre bezpapierové", "Access bridge for Paperless": "Prístupový mostík pre bezpapierové",
"Access bridge for Tandoor": "Prístupový mostík pre Tandoor", "Access bridge for Tandoor": "Prístupový mostík pre Tandoor",
"Access could not be granted; the application cannot write to:": "Prístup nebolo možné udeliť;aplikácia nemôže písať na:", "Access could not be granted; the application cannot write to:": "Prístup nebolo možné udeliť; aplikácia nemôže písať na:",
"Access granted to the application on:": "Prístup udelený aplikácii dňa:", "Access granted to the application on:": "Prístup udelený aplikácii dňa:",
"Access profile:": "Profil prístupu:", "Access profile:": "Profil prístupu:",
"Access token of the Jupyter Lab web interface": "Prístupový token webového rozhrania Jupyter Lab", "Access token of the Jupyter Lab web interface": "Prístupový token webového rozhrania Jupyter Lab",
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Tillgångsbro för Nextcloud", "Access bridge for Nextcloud": "Tillgångsbro för Nextcloud",
"Access bridge for Paperless": "Tillgångsbro för papperslös", "Access bridge for Paperless": "Tillgångsbro för papperslös",
"Access bridge for Tandoor": "Tillgångsbro för Tandoor", "Access bridge for Tandoor": "Tillgångsbro för Tandoor",
"Access could not be granted; the application cannot write to:": "Åtkomst kunde inte beviljas;ansökan kan inte skriva till:", "Access could not be granted; the application cannot write to:": "Åtkomst kunde inte beviljas; ansökan kan inte skriva till:",
"Access granted to the application on:": "Tillgång beviljad till ansökan på:", "Access granted to the application on:": "Tillgång beviljad till ansökan på:",
"Access profile:": "Åtkomstprofil:", "Access profile:": "Åtkomstprofil:",
"Access token of the Jupyter Lab web interface": "Tillgångstoken av Jupyter Labs webbgränssnitt", "Access token of the Jupyter Lab web interface": "Tillgångstoken av Jupyter Labs webbgränssnitt",
+1
View File
@@ -35,6 +35,7 @@
"tips": [ "tips": [
"Each directory to share is mounted under /shares with the name given to it during the installation; more can be added later with Modify.", "Each directory to share is mounted under /shares with the name given to it during the installation; more can be added later with Modify.",
"A directory of the host keeps the owner it already has. The panel shows whether the shared identity (1000:1000 by default) can write to it.", "A directory of the host keeps the owner it already has. The panel shows whether the shared identity (1000:1000 by default) can write to it.",
"Changing the shared identity (1000:1000) in Settings of the panel means giving the new one access on the host again: the volumes and the access rules are prepared for 1000:1000.",
"Create the Samba users in Settings of the panel, then publish each directory from Resources.", "Create the Samba users in Settings of the panel, then publish each directory from Resources.",
"NFS is served as NFSv3. NFSv4 needs the privileged profile offered by the advanced installation.", "NFS is served as NFSv3. NFSv4 needs the privileged profile offered by the advanced installation.",
"/config: Proxmox volume included in backup and kept when the application is updated or modified." "/config: Proxmox volume included in backup and kept when the application is updated or modified."
+1
View File
@@ -35,6 +35,7 @@
"tips": [ "tips": [
"Each directory to share is mounted under /shares with the name given to it during the installation; more can be added later with Modify.", "Each directory to share is mounted under /shares with the name given to it during the installation; more can be added later with Modify.",
"A directory of the host keeps the owner it already has. The panel shows whether the shared identity (1000:1000 by default) can write to it.", "A directory of the host keeps the owner it already has. The panel shows whether the shared identity (1000:1000 by default) can write to it.",
"Changing the shared identity (1000:1000) in Settings of the panel means giving the new one access on the host again: the volumes and the access rules are prepared for 1000:1000.",
"Create the Samba users in Settings of the panel, then publish each directory from Resources.", "Create the Samba users in Settings of the panel, then publish each directory from Resources.",
"NFS is served as NFSv3. NFSv4 needs the privileged profile offered by the advanced installation.", "NFS is served as NFSv3. NFSv4 needs the privileged profile offered by the advanced installation.",
"/config: Proxmox volume included in backup and kept when the application is updated or modified." "/config: Proxmox volume included in backup and kept when the application is updated or modified."
+29 -10
View File
@@ -1093,7 +1093,7 @@ apply_installer_profile() {
local generated_count preparation_count tls_count seeded_volumes mounted=0 failed=0 local generated_count preparation_count tls_count seeded_volumes mounted=0 failed=0
local rootfs="/var/lib/lxc/${VMID}/rootfs" local rootfs="/var/lib/lxc/${VMID}/rootfs"
local encoded item path mode owner destination target remove_lost_found owner_strategy local encoded item path mode owner destination target remove_lost_found owner_strategy
local only_when_mount_type selected_mount_type local only_when_mount_type selected_mount_type preparation_mode
local entrypoint working_directory halt_signal command_json compose_entrypoint_json user_spec local entrypoint working_directory halt_signal command_json compose_entrypoint_json user_spec
local supplemental_groups tls_config cert_path key_path cert_destination key_destination local supplemental_groups tls_config cert_path key_path cert_destination key_destination
local tls_directory valid_days common_name san local tls_directory valid_days common_name san
@@ -1198,6 +1198,13 @@ apply_installer_profile() {
*) oci_log "Unsupported owner strategy: $owner_strategy"; failed=1 ;; *) oci_log "Unsupported owner strategy: $owner_strategy"; failed=1 ;;
esac esac
(( failed == 0 )) || break (( failed == 0 )) || break
# A mode the preparation states, for a directory several users write to.
preparation_mode=$(jq -r '.mode // empty' <<<"$item")
if [[ -n $preparation_mode ]]; then
[[ $preparation_mode =~ ^[0-7]{3,4}$ ]] \
|| { oci_log "Invalid volume preparation mode: $preparation_mode"; failed=1; break; }
chmod "$preparation_mode" "${rootfs}${target}" || { failed=1; break; }
fi
oci_log "Volume prepared before the first start: $target" oci_log "Volume prepared before the first start: $target"
volumes_prepared=$((volumes_prepared + 1)) volumes_prepared=$((volumes_prepared + 1))
done < <(jq -r '.proxmox.installer_profile.volume_preparations[]? | @base64' "$TEMPLATE_FILE") done < <(jq -r '.proxmox.installer_profile.volume_preparations[]? | @base64' "$TEMPLATE_FILE")
@@ -1779,21 +1786,33 @@ fi
if [[ $SELINUX_LABEL_DISABLED == true ]]; then if [[ $SELINUX_LABEL_DISABLED == true ]]; then
oci_log "label:disable kept as metadata; Proxmox uses AppArmor, not SELinux, for this LXC" oci_log "label:disable kept as metadata; Proxmox uses AppArmor, not SELinux, for this LXC"
fi fi
# Gives the identity the application writes with access to a host directory # Gives the application access to a host directory that belongs to someone
# that belongs to someone else, with an ACL: the owner and the permissions of # else, with an ACL: the owner and the permissions of what is there stay as
# what is there stay as they are, and the owner of the directory keeps access # they are, and the owner of the directory keeps access to what the
# to what the application creates in it. # application creates in it. The rule names the identity the application
# writes with and, in an unprivileged container, its root user as well, which
# is who lists the folders and answers the access checks of an NFS client.
grant_shared_access() { grant_shared_access() {
local source=$1 owner group triplet default_spec local source=$1 owner group triplet spec default_spec
command -v setfacl >/dev/null 2>&1 && command -v getfacl >/dev/null 2>&1 || return 1 if ! command -v setfacl >/dev/null 2>&1 || ! command -v getfacl >/dev/null 2>&1; then
# A host installed on ZFS does not bring the ACL tools.
oci_log "Installing the acl package to grant access to $source"
DEBIAN_FRONTEND=noninteractive apt-get install -y acl >>"${OCI_LOG:-/dev/null}" 2>&1 || return 1
fi
spec="u:${HOST_BIND_UID}:rwX,g:${HOST_BIND_GID}:rwX"
default_spec="d:u:${HOST_BIND_UID}:rwx,d:g:${HOST_BIND_GID}:rwx"
if [[ $HOST_ROOT_UID != 0 ]]; then
spec="${spec},u:${HOST_ROOT_UID}:rwX,g:${HOST_ROOT_GID}:rwX"
default_spec="${default_spec},d:u:${HOST_ROOT_UID}:rwx,d:g:${HOST_ROOT_GID}:rwx"
fi
# Granted already: an update or a modification installs the container again. # Granted already: an update or a modification installs the container again.
if getfacl -cpn "$source" 2>/dev/null | grep -q "^user:${HOST_BIND_UID}:rwx" \ if getfacl -cpn "$source" 2>/dev/null | grep -q "^user:${HOST_BIND_UID}:rwx" \
&& getfacl -cpn "$source" 2>/dev/null | grep -q "^default:user:${HOST_BIND_UID}:rwx"; then && getfacl -cpn "$source" 2>/dev/null | grep -q "^default:user:${HOST_BIND_UID}:rwx" \
&& { [[ $HOST_ROOT_UID == 0 ]] || getfacl -cpn "$source" 2>/dev/null | grep -q "^user:${HOST_ROOT_UID}:rwx"; }; then
return 0 return 0
fi fi
owner=$(stat -c '%u' "$source") || return 1 owner=$(stat -c '%u' "$source") || return 1
group=$(stat -c '%g' "$source") || return 1 group=$(stat -c '%g' "$source") || return 1
default_spec="d:u:${HOST_BIND_UID}:rwx,d:g:${HOST_BIND_GID}:rwx"
if [[ $owner != 0 && $owner != "$HOST_BIND_UID" ]]; then if [[ $owner != 0 && $owner != "$HOST_BIND_UID" ]]; then
default_spec="${default_spec},d:u:${owner}:rwx" default_spec="${default_spec},d:u:${owner}:rwx"
fi fi
@@ -1804,7 +1823,7 @@ grant_shared_access() {
triplet=${triplet//S/-} triplet=${triplet//S/-}
default_spec="${default_spec},d:g:${group}:${triplet}" default_spec="${default_spec},d:g:${group}:${triplet}"
fi fi
setfacl -R -P -m "u:${HOST_BIND_UID}:rwX,g:${HOST_BIND_GID}:rwX" "$source" 2>>"${OCI_LOG:-/dev/null}" || return 1 setfacl -R -P -m "$spec" "$source" 2>>"${OCI_LOG:-/dev/null}" || return 1
find "$source" -xdev -type d -exec setfacl -m "$default_spec" {} + 2>>"${OCI_LOG:-/dev/null}" || return 1 find "$source" -xdev -type d -exec setfacl -m "$default_spec" {} + 2>>"${OCI_LOG:-/dev/null}" || return 1
} }
+3 -2
View File
@@ -176,9 +176,10 @@ def ask_shared_directories(ui, template: dict[str, Any], mounts: list[dict[str,
if advanced: if advanced:
mount["source"] = ask_storage(ui, f"{translate('Storage for')} {target}", "rootdir", storage) mount["source"] = ask_storage(ui, f"{translate('Storage for')} {target}", "rootdir", storage)
mount["size_gb"] = _ask_size(ui, target, int(declared.get("default_size_gb", 32))) mount["size_gb"] = _ask_size(ui, target, int(declared.get("default_size_gb", 32)))
# The application writes to it as its own user, not as root. # The application writes to it as its own user, not as root, and
# its group writes too: what is created inside keeps that group.
preparation = {"container_path": target, "remove_lost_found": True, preparation = {"container_path": target, "remove_lost_found": True,
"owner_strategy": "mapped-application-user", "owner_strategy": "mapped-application-user", "mode": "2775",
"only_when_mount_type": "managed-volume"} "only_when_mount_type": "managed-volume"}
if preparation not in profile.setdefault("volume_preparations", []): if preparation not in profile.setdefault("volume_preparations", []):
profile["volume_preparations"].append(preparation) profile["volume_preparations"].append(preparation)
+12 -1
View File
@@ -92,6 +92,10 @@ class SharedDirectoriesTests(unittest.TestCase):
prepared = {item["container_path"]: item["owner_strategy"] prepared = {item["container_path"]: item["owner_strategy"]
for item in template["proxmox"]["installer_profile"]["volume_preparations"]} for item in template["proxmox"]["installer_profile"]["volume_preparations"]}
self.assertEqual(prepared, {"/config": "mapped-root", "/shares/backups": "mapped-application-user"}) self.assertEqual(prepared, {"/config": "mapped-root", "/shares/backups": "mapped-application-user"})
# Its group writes as well, for when each Samba user acts as itself.
modes = {item["container_path"]: item.get("mode")
for item in template["proxmox"]["installer_profile"]["volume_preparations"]}
self.assertEqual(modes, {"/config": None, "/shares/backups": "2775"})
def test_a_default_installation_stays_unprivileged_and_generates_the_panel_password(self, *_): def test_a_default_installation_stays_unprivileged_and_generates_the_panel_password(self, *_):
ui, _template, plan = self.build({NAME: ["media"]}) ui, _template, plan = self.build({NAME: ["media"]})
@@ -194,7 +198,14 @@ class SharedDirectoriesTests(unittest.TestCase):
self.assertNotIn("chown", grant) self.assertNotIn("chown", grant)
self.assertNotIn("chmod", grant) self.assertNotIn("chmod", grant)
self.assertIn('grep -q "^default:user:${HOST_BIND_UID}:rwx"', grant) self.assertIn('grep -q "^default:user:${HOST_BIND_UID}:rwx"', grant)
self.assertIn('setfacl -R -P -m "u:${HOST_BIND_UID}:rwX,g:${HOST_BIND_GID}:rwX"', grant) self.assertIn('spec="u:${HOST_BIND_UID}:rwX,g:${HOST_BIND_GID}:rwX"', grant)
self.assertIn('setfacl -R -P -m "$spec" "$source"', grant)
# In an unprivileged container its root user is named too: it lists the folders.
self.assertIn('spec="${spec},u:${HOST_ROOT_UID}:rwX,g:${HOST_ROOT_GID}:rwX"', grant)
self.assertIn("apt-get install -y acl", grant)
# A preparation may state the mode of its directory.
self.assertIn('chmod "$preparation_mode" "${rootfs}${target}"', script)
self.assertIn("[[ $preparation_mode =~ ^[0-7]{3,4}$ ]]", script)
def test_a_name_that_cannot_identify_a_share_is_refused(self, *_): def test_a_name_that_cannot_identify_a_share_is_refused(self, *_):
for name in ("Media", "my files", "9lives", "config", "", "a" * 33): for name in ("Media", "my files", "9lives", "config", "", "a" * 33):