Keep the shell server's basic user journey working

The cleanup job now restores the whole home directory instead of only
the four text files: the directory itself, the shell configuration that
gives the prompt, and the files. Anything that is not a regular file is
replaced, so the files stay text files.

The README in server/ describes the security model: the box is a
disposable VPS that may be compromised, the root user may do whatever
they want, and only the prompt, the home directory and the four files
are kept working.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Zebreus 2026-10-25 13:02:25 +01:00
commit 23896bced5

View file

@ -1,6 +1,6 @@
# Installs the scheduled task that puts the shell server's README and Code of Conduct files back every 30
# minutes. The task runs as SYSTEM, so the shell server's root user can neither stop it nor change the copies it
# restores from.
# Installs the scheduled task that puts the shell server's home directory back into shape every 30 minutes: the
# prompt, the home directory and the four text files. The task runs as SYSTEM, so the shell server's root user can
# neither stop it nor change the copies it restores from.
#
# Run this in an elevated PowerShell on the server, with this script and the ../resources directory next to it:
# powershell -ExecutionPolicy Bypass -File .\install_restore_task.ps1
@ -8,36 +8,83 @@
# powershell -ExecutionPolicy Bypass -File .\install_restore_task.ps1 -Uninstall
[CmdletBinding()]
param(
[string]$Target = 'C:\msys2\home\root',
[string]$HomeDir = 'C:\msys2\home\root',
[switch]$Uninstall
)
$ErrorActionPreference = 'Stop'
$installDir = 'C:\ProgramData\kik-restore'
$copiesDir = Join-Path $installDir 'files'
$taskName = 'KiK restore shell files'
$files = 'README', 'README.en', 'CODE_OF_CONDUCT', 'CODE_OF_CONDUCT.en'
# The copies the task restores from and the script it runs both live in $installDir, which only SYSTEM and the
# administrators may read or write.
$restoreScript = @'
# Puts the shell server's README and Code of Conduct files back from the copies next to this script.
# Runs as SYSTEM every 30 minutes; the shell server's root user can neither stop it nor change the copies.
param([string]$Target = 'C:\msys2\home\root')
# Puts the shell server's home directory back into shape: the directory itself, the shell configuration that gives
# the prompt, and the four text files. Runs as SYSTEM every 30 minutes; the shell server's root user can neither stop
# it nor change the copies it restores from.
param([string]$HomeDir = 'C:\msys2\home\root')
$log = Join-Path $PSScriptRoot 'restore.log'
$copies = Join-Path $PSScriptRoot 'files'
foreach ($name in @('README', 'README.en', 'CODE_OF_CONDUCT', 'CODE_OF_CONDUCT.en')) {
$path = Join-Path $Target $name
# The account of the shell server's root user, whose id the files in its home directory belong to.
$passwd = Get-Content 'C:\msys2\etc\passwd' -ErrorAction SilentlyContinue | Where-Object { $_ -like 'root:*' }
$sid = if ($passwd) { ($passwd -split ':')[4].Split(',')[-1] }
function Write-Log($message) {
"$(Get-Date -Format s) $message" | Out-File -FilePath $log -Append
}
# Removes a file, a directory or a link, but never what a link points to.
function Remove-Anything($path) {
$item = Get-Item -LiteralPath $path -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
if ($item.PSIsContainer) { [IO.Directory]::Delete($path) } else { [IO.File]::Delete($path) }
} elseif ($item.PSIsContainer) {
Remove-Item -LiteralPath $path -Recurse -Force
} else {
Remove-Item -LiteralPath $path -Force
}
}
# The home directory is the one from /etc/passwd, which the user cannot change, but which they can delete.
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
}
}
if (-not (Test-Path -LiteralPath $HomeDir)) {
New-Home $HomeDir
Write-Log "created $HomeDir"
} elseif ((Get-Item -LiteralPath $HomeDir -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) {
Remove-Anything $HomeDir
New-Home $HomeDir
Write-Log "replaced the link $HomeDir"
}
foreach ($copy in Get-ChildItem -LiteralPath $copies -File -Force) {
$target = Join-Path $HomeDir $copy.Name
try {
if (Test-Path -LiteralPath $path) {
# the root user may have replaced the file or locked everyone else out
takeown /f $path /a | Out-Null
icacls $path /reset | Out-Null
if ((Get-Item -LiteralPath $path -Force).PSIsContainer) { Remove-Item -LiteralPath $path -Recurse -Force }
if (Test-Path -LiteralPath $target) {
$item = Get-Item -LiteralPath $target -Force
if ($item.PSIsContainer -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
# anything but a file is replaced, so that the files are always the text files they should be
Remove-Anything $target
} 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
}
}
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination $path -Force
"$(Get-Date -Format s) restored $name" | Out-File -FilePath $log -Append
Copy-Item -LiteralPath $copy.FullName -Destination $target -Force
if ($sid) { icacls $target /setowner "*$sid" | Out-Null }
Write-Log "restored $($copy.Name)"
} catch {
"$(Get-Date -Format s) failed $name : $_" | Out-File -FilePath $log -Append
Write-Log "failed $($copy.Name) : $_"
}
}
'@
@ -54,14 +101,13 @@ if ($Uninstall) {
}
$resources = Join-Path $PSScriptRoot '..\resources'
$missing = $files | Where-Object { -not (Test-Path -LiteralPath (Join-Path $resources $_)) }
if ($missing) { throw "Missing files in $resources : $($missing -join ', ')" }
if (-not (Get-ChildItem -LiteralPath $resources -File -Force)) { throw "No files in $resources." }
# The root user may have prepared the directory or the file names, so nothing that looks like a link is used.
$prepared = Get-Item -LiteralPath $installDir -Force -ErrorAction SilentlyContinue
if ($prepared -and $prepared.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$installDir is a link, refusing to install." }
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
New-Item -ItemType Directory -Force -Path $copiesDir | Out-Null
# Only SYSTEM and the administrators may read or write the copies and the script.
$acl = New-Object System.Security.AccessControl.DirectorySecurity
@ -73,18 +119,18 @@ $acl.SetOwner((New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32
Set-Acl -LiteralPath $installDir -AclObject $acl
# Files the root user prepared are removed, the copies are written fresh and inherit the restricted permissions.
foreach ($name in $files + 'restore.ps1') {
$path = Join-Path $installDir $name
foreach ($copy in Get-ChildItem -LiteralPath $resources -File -Force) {
$path = Join-Path $copiesDir $copy.Name
if (Test-Path -LiteralPath $path) {
$item = Get-Item -LiteralPath $path -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$path is a link, refusing to install." }
Remove-Item -LiteralPath $path -Recurse -Force
}
Copy-Item -LiteralPath $copy.FullName -Destination $path -Force
}
Copy-Item -Path (Join-Path $resources '*') -Destination $installDir -Force
[IO.File]::WriteAllText((Join-Path $installDir 'restore.ps1'), $restoreScript)
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\restore.ps1`" -Target `"$Target`""
$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)
New-ScheduledTaskTrigger -AtStartup
@ -97,5 +143,5 @@ Start-ScheduledTask -TaskName $taskName
Start-Sleep -Seconds 5
Write-Host "Task '$taskName' runs as SYSTEM every 30 minutes and at startup."
Get-ScheduledTaskInfo -TaskName $taskName | Format-List LastRunTime, LastTaskResult, NextRunTime
Get-Content (Join-Path $installDir 'restore.log') -Tail 4 -ErrorAction SilentlyContinue
Get-Content (Join-Path $installDir 'restore.log') -Tail 8 -ErrorAction SilentlyContinue
icacls $installDir