From bc7cb26b61323e532779fc8dd0d1ef82b4c65a39 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Sun, 25 Oct 2026 15:04:48 +0100 Subject: [PATCH] Keep the home directory and the files in it from being deleted The user could delete the whole home directory, which takes the website down with it: sshd cannot log in without it. The home directory and the files that have to stay in it now belong to the administrators, so the user may change them but not remove them. Their own files stay theirs. Co-Authored-By: Claude Opus 5.5 (1M context) --- server/README.md | 12 +++--- server/scripts/install_restore_task.ps1 | 49 +++++++++++++++++++------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/server/README.md b/server/README.md index 178b8dc..48ccfe4 100644 --- a/server/README.md +++ b/server/README.md @@ -17,17 +17,18 @@ We only keep the basic user journey working. ## What we keep working - The prompt: `root@khaos-im-kiez:~#`, which `~/.zshrc` sets. -- The home directory the user ends up in: `/home/root`. They can delete it, but not change where it is. +- The home directory the user ends up in: `/home/root`. They cannot change where it is, and they cannot delete it. - `cat README`, `cat README.en`, `cat CODE_OF_CONDUCT` and `cat CODE_OF_CONDUCT.en` always work. The four files are always there and always regular text files. - The website can log in: the key it uses is in `~/.ssh/authorized_keys`. That key gets access to the `root` user only, never to the Administrator account. -Modifying the four files is allowed; the next cleanup undoes it. Deleting them is not permanent either. +Modifying the four files is allowed; the next cleanup undoes it. Deleting them is not possible: the home directory +and the files that have to stay in it belong to the administrators, so the user cannot remove them. The files the +user creates are their own; they may change and delete them. The user may add keys of their own to `authorized_keys`; they stay. The key of the website is put back, so that the -site keeps working. The user owns their home directory, so they can always delete the file - nothing on Windows can -stop them from that - but the next cleanup brings it back. Until then, the site cannot log in. +site keeps working. ## What we do not keep working @@ -46,7 +47,8 @@ administrators. Neither the setup nor the cleanup ever runs as the `root` user. It creates `/home/root` if it is gone, and puts the copies from `C:\ProgramData\kik-restore\files` back into the home directory: the shell configuration, the keys and the four text files. Anything that is not a regular file is -replaced, so that the files cannot become anything but text. +replaced, so that the files cannot become anything but text. It also sets the permissions of the home directory and +of those files, so that the user cannot delete them. ## Installing the cleanup on the server diff --git a/server/scripts/install_restore_task.ps1 b/server/scripts/install_restore_task.ps1 index 5d8b122..b2aafed 100644 --- a/server/scripts/install_restore_task.ps1 +++ b/server/scripts/install_restore_task.ps1 @@ -59,15 +59,44 @@ function Restore-Keys($target, $source) { [IO.File]::WriteAllText($target, (($keys + $required) -join "`n") + "`n") } +# The permissions of the home directory and of the files in it that have to stay: the user may read and write them +# and delete their own files, but not the home directory itself and not the files the website needs. Only SYSTEM +# and the administrators own them, so the user cannot take the permissions away again. +function Set-ProtectedAcl($path, $forDirectory) { + if (-not $sid) { return } + try { + $system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18') + $admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') + $user = New-Object System.Security.Principal.SecurityIdentifier($sid) + $acl = Get-Acl -LiteralPath $path + $acl.SetAccessRuleProtection($true, $false) + foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) } + $acl.SetOwner($admins) + $none = [System.Security.AccessControl.InheritanceFlags]::None + $children = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit + $noPropagate = [System.Security.AccessControl.PropagationFlags]::None + $inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly + $full = [System.Security.AccessControl.FileSystemRights]::FullControl + $some = [System.Security.AccessControl.FileSystemRights]::ReadAndExecute -bor [System.Security.AccessControl.FileSystemRights]::Write + $inherit = if ($forDirectory) { $children } else { $none } + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $system, $full, $inherit, $noPropagate, 'Allow')) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $admins, $full, $inherit, $noPropagate, 'Allow')) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $some, $none, $noPropagate, 'Allow')) + if ($forDirectory) { + # the files in it are the user's own, they may delete them + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $full, $children, $inheritOnly, 'Allow')) + } + Set-Acl -LiteralPath $path -AclObject $acl + } catch { + Write-Log "the permissions of $path are not set: $_" + } +} + # The home directory is the one from /etc/passwd, which the user cannot change, but which they can delete. A new one # gets MSYS2's default files from /etc/skel, which MSYS2 itself only copies into a home that does not exist yet. function New-Home($path) { New-Item -ItemType Directory -Path $path | Out-Null - # The parent directory gives the users read access only, so the owner gets write access on their own home. - if ($sid) { - icacls $path /grant "*${sid}:(OI)(CI)F" | Out-Null - icacls $path /setowner "*$sid" | Out-Null - } + Set-ProtectedAcl $path $true Get-ChildItem -LiteralPath 'C:\msys2\etc\skel' -Force | Copy-Item -Destination $path -Recurse -Force } @@ -78,6 +107,8 @@ if (-not (Test-Path -LiteralPath $HomeDir)) { Remove-Anything $HomeDir New-Home $HomeDir Write-Log "replaced the link $HomeDir" +} else { + Set-ProtectedAcl $HomeDir $true } foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) { @@ -88,10 +119,7 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) { Remove-Anything $dir } if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null } - if ($sid) { - icacls $dir /grant "*${sid}:(OI)(CI)F" | Out-Null - icacls $dir /setowner "*$sid" | Out-Null - } + Set-ProtectedAcl $dir $true if (Test-Path -LiteralPath $target) { $item = Get-Item -LiteralPath $target -Force if ($item.PSIsContainer -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) { @@ -100,7 +128,6 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) { } else { # the user may have locked the file; taking it back lets us write it takeown /f $target /a | Out-Null - icacls $target /reset | Out-Null } } if ($copy.Name -eq 'authorized_keys') { @@ -108,7 +135,7 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) { } else { Copy-Item -LiteralPath $copy.FullName -Destination $target -Force } - if ($sid) { icacls $target /setowner "*$sid" | Out-Null } + Set-ProtectedAcl $target $false Write-Log "restored $($copy.Name)" } catch { Write-Log "failed $($copy.Name) : $_"