From 49c61b6b9116baf3af0e2123d0e77da7d41fe8fe Mon Sep 17 00:00:00 2001 From: Zebreus Date: Sun, 25 Oct 2026 15:26:01 +0100 Subject: [PATCH] Keep the shell configuration and the key out of the user's reach The user could empty the authorized_keys and break the login of the website, change the shell configuration to break the prompt, and write startup files that zsh reads on every login. The shell configuration and the key are now read-only for the user, and the shell reads its configuration from ~/.zsh, so the files in the home directory are ignored. Co-Authored-By: Claude Opus 5.5 (1M context) --- server/README.md | 6 +++++- server/resources/home/{ => .zsh}/.zshrc | 0 server/resources/home/.zshenv | 4 ++++ server/scripts/install_restore_task.ps1 | 28 +++++++++++++++++-------- 4 files changed, 28 insertions(+), 10 deletions(-) rename server/resources/home/{ => .zsh}/.zshrc (100%) diff --git a/server/README.md b/server/README.md index 84278b2..ef16285 100644 --- a/server/README.md +++ b/server/README.md @@ -19,7 +19,7 @@ We only keep the basic user journey working. Anything that breaks the following user journey is not acceptable: - The user can connect with the SSH key as `root`. -- The prompt: `root@khaos-im-kiez:~#`, which `~/.zshrc` sets. +- The prompt: `root@khaos-im-kiez:~#`, which `~/.zsh/.zshrc` sets. - 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. @@ -41,6 +41,10 @@ The scheduled task `KiK restore shell files` runs every 30 minutes and at every The second job should also be able to restore a new server to the expected state. +The files that have to stay belong to the administrators, so the user cannot delete them, and they cannot change the +shell configuration or the key the website logs in with either. The shell reads its configuration from `~/.zsh`, so +the startup files the user could write in the home directory are ignored. + ## Installing the cleanup on the server To deploy the cleanup task an the server, run: diff --git a/server/resources/home/.zshrc b/server/resources/home/.zsh/.zshrc similarity index 100% rename from server/resources/home/.zshrc rename to server/resources/home/.zsh/.zshrc diff --git a/server/resources/home/.zshenv b/server/resources/home/.zshenv index f990c34..3625bba 100644 --- a/server/resources/home/.zshenv +++ b/server/resources/home/.zshenv @@ -1,3 +1,7 @@ # Preset what MSYS2's /etc/profile would otherwise start a helper process for (~90 ms each here). # TZ has to match the Windows timezone (W. Europe Standard Time). export LANG=en_US.UTF-8 TZ=Europe/Berlin + +# The shell reads its configuration from here, not from the home directory: the user may not change it, and the +# files in the home directory that zsh would read otherwise (.zprofile, .zlogin) are ignored. +export ZDOTDIR=~/.zsh diff --git a/server/scripts/install_restore_task.ps1 b/server/scripts/install_restore_task.ps1 index b2aafed..6dd6ced 100644 --- a/server/scripts/install_restore_task.ps1 +++ b/server/scripts/install_restore_task.ps1 @@ -59,10 +59,13 @@ 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) { +# The shell configuration and the key the website logs in with must not change, the four texts may be edited. +$readOnly = '.zshrc', '.zshenv', 'authorized_keys' + +# The permissions of the home directory and of the files in it that have to stay: the user may read them and write +# the ones that may change, 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, $writable) { if (-not $sid) { return } try { $system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18') @@ -78,10 +81,12 @@ function Set-ProtectedAcl($path, $forDirectory) { $inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly $full = [System.Security.AccessControl.FileSystemRights]::FullControl $some = [System.Security.AccessControl.FileSystemRights]::ReadAndExecute -bor [System.Security.AccessControl.FileSystemRights]::Write + $read = [System.Security.AccessControl.FileSystemRights]::ReadAndExecute $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')) + $userRights = if ($writable) { $some } else { $read } + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $userRights, $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')) @@ -96,7 +101,7 @@ function Set-ProtectedAcl($path, $forDirectory) { # 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 - Set-ProtectedAcl $path $true + Set-ProtectedAcl $path $true $true Get-ChildItem -LiteralPath 'C:\msys2\etc\skel' -Force | Copy-Item -Destination $path -Recurse -Force } @@ -108,7 +113,7 @@ if (-not (Test-Path -LiteralPath $HomeDir)) { New-Home $HomeDir Write-Log "replaced the link $HomeDir" } else { - Set-ProtectedAcl $HomeDir $true + Set-ProtectedAcl $HomeDir $true $true } foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) { @@ -119,7 +124,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 } - Set-ProtectedAcl $dir $true + Set-ProtectedAcl $dir $true $true if (Test-Path -LiteralPath $target) { $item = Get-Item -LiteralPath $target -Force if ($item.PSIsContainer -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) { @@ -135,7 +140,7 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) { } else { Copy-Item -LiteralPath $copy.FullName -Destination $target -Force } - Set-ProtectedAcl $target $false + Set-ProtectedAcl $target $false ($readOnly -notcontains $copy.Name) Write-Log "restored $($copy.Name)" } catch { Write-Log "failed $($copy.Name) : $_" @@ -192,6 +197,11 @@ if (-not ($keys | Where-Object { ($_ -split '\s+')[1] -eq ($required[0] -split ' } [IO.File]::WriteAllText((Join-Path $installDir 'restore.ps1'), $restoreScript) +# The shell configuration used to be in the home directory, the shell reads it from ~/.zsh now. +if (Test-Path -LiteralPath (Join-Path $HomeDir '.zsh\.zshrc')) { + Remove-Item -LiteralPath (Join-Path $HomeDir '.zshrc') -Force -ErrorAction SilentlyContinue +} + $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\restore.ps1`" -HomeDir `"$HomeDir`"" $triggers = @( New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30) -RepetitionDuration (New-TimeSpan -Days 3650)