Restore the SSH access in the cleanup

The key that the website logs in with lives in the home directory of the
user, so a deleted home directory took it away and the site could not
connect any more. The key is now one of the copies the cleanup restores,
and it is merged into the authorized_keys of the user, so that keys they
added stay.

The installer writes the same key for the Administrator, and the deploy
script checks that it is the key the website uses.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Zebreus 2026-10-25 13:35:05 +01:00
commit 22106dcd7d
4 changed files with 106 additions and 28 deletions

View file

@ -36,6 +36,14 @@ if [ -z "$KEYFILE" ]; then
printf '%s' "$boring" | base64 -d | tr 'A-Za-z' 'N-ZA-Mn-za-m' > "$KEYFILE"
fi
# The site logs in with the key in the authorized_keys of the user, so the two have to be the same.
public=$(ssh-keygen -y -f "$KEYFILE" | awk 'NF {print $1" "$2}')
authorized=$(awk 'NF {print $1" "$2; exit}' "$server/resources/.ssh/authorized_keys")
if [ "$public" != "$authorized" ]; then
echo "the key in server/resources/.ssh/authorized_keys is not the one the website uses" >&2
exit 1
fi
# The host key is the one the website pins, so that the deployment cannot be intercepted.
expected=$(sed -n "s/.*const HOST_KEY = '\([^']*\)'.*/\1/p" "$repo/src/scripts/terminal.ts")
known=$(mktemp)

View file

@ -21,8 +21,8 @@ $taskName = 'KiK restore shell files'
# administrators may read or write.
$restoreScript = @'
# 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.
# the prompt, the keys that let the website in, 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'
@ -47,8 +47,21 @@ function Remove-Anything($path) {
}
}
# The home directory is the one from /etc/passwd, which the user cannot change, but which they can delete.
function New-Home($path) {
# The website logs in with a key in the authorized_keys of the user. The user may keep keys of their own, so only
# the key of the website is put back.
function Restore-Keys($target, $source) {
$required = @(Get-Content -LiteralPath $source | Where-Object { $_.Trim() })
$blob = ($required[0] -split '\s+')[1]
$keys = @()
if (Test-Path -LiteralPath $target) {
$keys = @(Get-Content -LiteralPath $target | Where-Object { $_.Trim() -and ($_ -split '\s+')[1] -ne $blob })
}
[IO.File]::WriteAllText($target, (($keys + $required) -join "`n") + "`n")
}
# The home directory is the one from /etc/passwd, which the user cannot change, but which they can delete. The
# directories in it belong to the user, so that they can put their own keys in .ssh.
function New-Dir($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) {
@ -58,17 +71,26 @@ function New-Home($path) {
}
if (-not (Test-Path -LiteralPath $HomeDir)) {
New-Home $HomeDir
New-Dir $HomeDir
Write-Log "created $HomeDir"
} elseif ((Get-Item -LiteralPath $HomeDir -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) {
Remove-Anything $HomeDir
New-Home $HomeDir
New-Dir $HomeDir
Write-Log "replaced the link $HomeDir"
}
foreach ($copy in Get-ChildItem -LiteralPath $copies -File -Force) {
$target = Join-Path $HomeDir $copy.Name
foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) {
$target = Join-Path $HomeDir $copy.FullName.Substring($copies.Length).TrimStart('\')
try {
$dir = Split-Path -Parent $target
if ((Test-Path -LiteralPath $dir) -and -not (Get-Item -LiteralPath $dir -Force).PSIsContainer) {
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
}
if (Test-Path -LiteralPath $target) {
$item = Get-Item -LiteralPath $target -Force
if ($item.PSIsContainer -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
@ -80,7 +102,11 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -File -Force) {
icacls $target /reset | Out-Null
}
}
Copy-Item -LiteralPath $copy.FullName -Destination $target -Force
if ($copy.Name -eq 'authorized_keys') {
Restore-Keys $target $copy.FullName
} else {
Copy-Item -LiteralPath $copy.FullName -Destination $target -Force
}
if ($sid) { icacls $target /setowner "*$sid" | Out-Null }
Write-Log "restored $($copy.Name)"
} catch {
@ -113,20 +139,29 @@ New-Item -ItemType Directory -Force -Path $copiesDir | Out-Null
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
foreach ($sid in 'S-1-5-18', 'S-1-5-32-544') {
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
$identity = New-Object System.Security.Principal.SecurityIdentifier $sid
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
}
$acl.SetOwner((New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32-544'))
Set-Acl -LiteralPath $installDir -AclObject $acl
# Files the root user prepared are removed, the copies are written fresh and inherit the restricted permissions.
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
foreach ($item in Get-ChildItem -LiteralPath $copiesDir -Recurse -Force) {
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$($item.FullName) is a link, refusing to install." }
}
Remove-Item -LiteralPath $copiesDir -Recurse -Force
New-Item -ItemType Directory -Force -Path $copiesDir | Out-Null
Copy-Item -Path (Join-Path $resources '*') -Destination $copiesDir -Recurse -Force
# The Administrator logs in with the same key, so that the deployment keeps working. The keys that are already
# there stay.
$adminKeys = 'C:\Users\Administrator\.ssh\authorized_keys'
$required = @(Get-Content -LiteralPath (Join-Path $resources '.ssh\authorized_keys') | Where-Object { $_.Trim() })
$keys = @()
if (Test-Path -LiteralPath $adminKeys) { $keys = @(Get-Content -LiteralPath $adminKeys | Where-Object { $_.Trim() }) }
if (-not ($keys | Where-Object { ($_ -split '\s+')[1] -eq ($required[0] -split '\s+')[1] })) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $adminKeys) | Out-Null
[IO.File]::WriteAllText($adminKeys, (($keys + $required) -join "`n") + "`n")
}
[IO.File]::WriteAllText((Join-Path $installDir 'restore.ps1'), $restoreScript)

View file

@ -1,6 +1,6 @@
# Builds the files for the home directory of the shell server's root user in ../resources: README, README.en,
# CODE_OF_CONDUCT and CODE_OF_CONDUCT.en as the website's pages in terminal text with escape sequences, and the
# shell configuration from ../shell.
# files of ../home, which are copied as they are.
# Usage: python3 server/scripts/make_files.py
import pathlib
import shutil
@ -336,6 +336,12 @@ out.mkdir(exist_ok=True)
for name, text in [('README', readme(DE)), ('README.en', readme(EN)),
('CODE_OF_CONDUCT', code_of_conduct(COC_DE)), ('CODE_OF_CONDUCT.en', code_of_conduct(COC_EN))]:
(out / name).write_text(text, encoding='utf-8')
# The shell configuration is not generated, it is copied from ../shell, where it is edited.
for name in '.zshrc', '.zshenv':
shutil.copyfile(pathlib.Path(__file__).parent.parent / 'shell' / name, out / name)
# The files for the home directory are not generated, they are copied from ../home, where they are edited.
home = pathlib.Path(__file__).parent.parent / 'home'
for path in home.rglob('*'):
target = out / path.relative_to(home)
if path.is_dir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, target)

View file

@ -0,0 +1,13 @@
@echo off
REM Puts the SSH access back that a deleted home directory took with it: the key the website logs in with, in the
REM home of the root user and in the home of the Administrator. Run it as Administrator on the box, for example
REM over RDP. The cleanup job puts the key back by itself from then on.
set KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDweEH8bhZAHVaAedhTVDb2j/oMEn3vm97xCwCw9LC9 root@nowhere.wirs.ing cygwin-2222
if not exist C:\msys2\home\root\.ssh mkdir C:\msys2\home\root\.ssh
> C:\msys2\home\root\.ssh\authorized_keys echo %KEY%
if not exist C:\Users\Administrator\.ssh mkdir C:\Users\Administrator\.ssh
>> C:\Users\Administrator\.ssh\authorized_keys echo %KEY%
echo The SSH access is back.