mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
fix(oci): the access rule covers the container's root, and a shared volume is writable by its group
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user