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 Paperless": "Access Bridge für Paperless",
"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 profile:": "Zugangsprofil:",
"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 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 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",
"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",
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Pont d'accès pour Nextcloud",
"Access bridge for Paperless": "Pont d'accès pour sans papier",
"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 profile:": "Profil d'accès :",
"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 Paperless": "Bridge di accesso per Paperless",
"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 profile:": "Profilo di accesso:",
"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 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 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 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.",
+1 -1
View File
@@ -190,7 +190,7 @@
"Access bridge for Nextcloud": "Ponte de acesso para Nextcloud",
"Access bridge for Paperless": "Ponte de acesso para Paperless",
"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 profile:": "Perfil de acesso:",
"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 Paperless": "Prístupový mostík pre bezpapierové",
"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 profile:": "Profil prístupu:",
"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 Paperless": "Tillgångsbro för papperslös",
"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 profile:": "Åtkomstprofil:",
"Access token of the Jupyter Lab web interface": "Tillgångstoken av Jupyter Labs webbgränssnitt",
+1
View File
@@ -35,6 +35,7 @@
"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.",
"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.",
"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."
+1
View File
@@ -35,6 +35,7 @@
"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.",
"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.",
"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."
+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 rootfs="/var/lib/lxc/${VMID}/rootfs"
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 supplemental_groups tls_config cert_path key_path cert_destination key_destination
local tls_directory valid_days common_name san
@@ -1198,6 +1198,13 @@ apply_installer_profile() {
*) oci_log "Unsupported owner strategy: $owner_strategy"; failed=1 ;;
esac
(( 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"
volumes_prepared=$((volumes_prepared + 1))
done < <(jq -r '.proxmox.installer_profile.volume_preparations[]? | @base64' "$TEMPLATE_FILE")
@@ -1779,21 +1786,33 @@ fi
if [[ $SELINUX_LABEL_DISABLED == true ]]; then
oci_log "label:disable kept as metadata; Proxmox uses AppArmor, not SELinux, for this LXC"
fi
# Gives the identity the application writes with access to a host directory
# that belongs to someone else, with an ACL: the owner and the permissions of
# what is there stay as they are, and the owner of the directory keeps access
# to what the application creates in it.
# Gives the application access to a host directory that belongs to someone
# else, with an ACL: the owner and the permissions of what is there stay as
# they are, and the owner of the directory keeps access to what the
# 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() {
local source=$1 owner group triplet default_spec
command -v setfacl >/dev/null 2>&1 && command -v getfacl >/dev/null 2>&1 || return 1
local source=$1 owner group triplet spec default_spec
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.
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
fi
owner=$(stat -c '%u' "$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
default_spec="${default_spec},d:u:${owner}:rwx"
fi
@@ -1804,7 +1823,7 @@ grant_shared_access() {
triplet=${triplet//S/-}
default_spec="${default_spec},d:g:${group}:${triplet}"
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
}
+3 -2
View File
@@ -176,9 +176,10 @@ def ask_shared_directories(ui, template: dict[str, Any], mounts: list[dict[str,
if advanced:
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)))
# 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,
"owner_strategy": "mapped-application-user",
"owner_strategy": "mapped-application-user", "mode": "2775",
"only_when_mount_type": "managed-volume"}
if preparation not in profile.setdefault("volume_preparations", []):
profile["volume_preparations"].append(preparation)
+12 -1
View File
@@ -92,6 +92,10 @@ class SharedDirectoriesTests(unittest.TestCase):
prepared = {item["container_path"]: item["owner_strategy"]
for item in template["proxmox"]["installer_profile"]["volume_preparations"]}
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, *_):
ui, _template, plan = self.build({NAME: ["media"]})
@@ -194,7 +198,14 @@ class SharedDirectoriesTests(unittest.TestCase):
self.assertNotIn("chown", grant)
self.assertNotIn("chmod", 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, *_):
for name in ("Media", "my files", "9lives", "config", "", "a" * 33):