From fc7b75b930ac5c44e2d24a082fbaefbcfdd8f112 Mon Sep 17 00:00:00 2001 From: Zebreus Date: Sun, 25 Oct 2026 13:50:40 +0100 Subject: [PATCH] Keep the key of the website away from the Administrator account The key that the website logs in with was written into the authorized_keys of the Administrator, which would have given everyone who has the website administrator rights on the box. It only gets access as the root user. The deploy script logs in as Administrator with the personal key of the deployer instead, from the agent or from -i. Co-Authored-By: Claude Code --- server/README.md | 15 +++++----- server/scripts/deploy.sh | 39 +++++++++++++------------ server/scripts/install_restore_task.ps1 | 11 ------- server/scripts/restore_ssh_access.cmd | 7 ++--- 4 files changed, 30 insertions(+), 42 deletions(-) diff --git a/server/README.md b/server/README.md index b8c2b1c..549af3d 100644 --- a/server/README.md +++ b/server/README.md @@ -21,7 +21,8 @@ We only keep the basic user journey working. which the user cannot change, because `/etc` is read-only for them. They can delete their home directory, though. - `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`. +- 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, whose keys belong to the people who deploy. Modifying the four files is allowed; the next cleanup undoes it. Deleting them is not permanent either. @@ -77,9 +78,10 @@ The resources are built from the website's pages and the files of `home/`: server/scripts/deploy.sh The script logs in as `Administrator` - whose shell is a Windows one - copies the scripts and the resources to the -Administrator's home directory and runs the installer there. It uses the key the website uses, which is authorized -for the Administrator account as well, checks the host key against the one the website pins, and checks that the key -in `home/.ssh/authorized_keys` is the one the website uses. `--uninstall` removes the task again. +Administrator's home directory and runs the installer there. It logs in with the personal key of the deployer, from +the agent or from `-i`; the key of the website never gets administrator rights, it only logs in as the root user. +It checks the host key against the one the website pins, and checks that the key in `home/.ssh/authorized_keys` is +the one the website uses. `--uninstall` removes the task again. The installer can also be run by hand, in an elevated PowerShell on the server: @@ -90,6 +92,5 @@ runs at startup. Remove it again with `-Uninstall`. ## Getting the SSH access back -If the home directory was deleted, the key that lets the website in went with it, and so did the Administrator's. -Over RDP, as Administrator, run `scripts/restore_ssh_access.cmd`. From then on the cleanup puts the key back by -itself. +If the home directory was deleted, the key that lets the website in went with it. Over RDP, as Administrator, run +`scripts/restore_ssh_access.cmd`. From then on the cleanup puts the key back by itself. diff --git a/server/scripts/deploy.sh b/server/scripts/deploy.sh index f115bb6..5a312c7 100755 --- a/server/scripts/deploy.sh +++ b/server/scripts/deploy.sh @@ -5,6 +5,9 @@ # # The job runs on the box as SYSTEM, never as the root user. The Administrator account sets it up; its shell is a # Windows one, so the commands we run there are Windows commands. +# +# The Administrator logs in with the personal key of the deployer, from the agent or from -i. The key of the website +# never gets administrator rights: it only logs in as the root user. set -euo pipefail HOST=nowhere.wirs.ing @@ -27,23 +30,6 @@ remote='C:\Users\Administrator\kik' # where the installer is put, in the home remotePath='/c/Users/Administrator/kik' # the same place, as the file transfer sees it temporary=() -if [ -z "$KEYFILE" ]; then - # The key the website uses is authorized for the Administrator account as well. - KEYFILE=$(mktemp) - chmod 600 "$KEYFILE" - temporary+=("$KEYFILE") - boring=$(sed -n "s/.*const boringData = '\([^']*\)'.*/\1/p" "$repo/src/scripts/terminal.ts") - 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) @@ -56,8 +42,23 @@ if [ "$actual" != "$expected" ]; then exit 1 fi -ssh=(ssh -i "$KEYFILE" -o UserKnownHostsFile="$known" -o StrictHostKeyChecking=yes "$USER@$HOST") -scp=(scp -i "$KEYFILE" -o UserKnownHostsFile="$known" -o StrictHostKeyChecking=yes) +# The site logs in with the key in the authorized_keys of the user on the box, so the two have to be the same. +site=$(mktemp) +chmod 600 "$site" +temporary+=("$site") +boring=$(sed -n "s/.*const boringData = '\([^']*\)'.*/\1/p" "$repo/src/scripts/terminal.ts") +printf '%s' "$boring" | base64 -d | tr 'A-Za-z' 'N-ZA-Mn-za-m' > "$site" +public=$(ssh-keygen -y -f "$site" | 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 + +options=(-o "UserKnownHostsFile=$known" -o StrictHostKeyChecking=yes) +if [ -n "$KEYFILE" ]; then options+=(-i "$KEYFILE"); fi +ssh=(ssh "${options[@]}" "$USER@$HOST") +scp=(scp "${options[@]}") if [ "$UNINSTALL" = 1 ]; then "${ssh[@]}" "if exist $remote\\scripts\\install_restore_task.ps1 (powershell -NoProfile -ExecutionPolicy Bypass -File $remote\\scripts\\install_restore_task.ps1 -Uninstall) else (echo Nothing is installed.)" diff --git a/server/scripts/install_restore_task.ps1 b/server/scripts/install_restore_task.ps1 index 2e56660..003b27f 100644 --- a/server/scripts/install_restore_task.ps1 +++ b/server/scripts/install_restore_task.ps1 @@ -152,17 +152,6 @@ foreach ($item in Get-ChildItem -LiteralPath $copiesDir -Recurse -Force) { 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) $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\restore.ps1`" -HomeDir `"$HomeDir`"" diff --git a/server/scripts/restore_ssh_access.cmd b/server/scripts/restore_ssh_access.cmd index 63670a2..188d7b4 100644 --- a/server/scripts/restore_ssh_access.cmd +++ b/server/scripts/restore_ssh_access.cmd @@ -1,16 +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. +REM home of the root user. Run it as Administrator on the box, for example over RDP. The cleanup job puts the key +REM back by itself from then on. setlocal EnableDelayedExpansion 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% - REM The user owns their .ssh, so that they can add keys of their own. for /f "tokens=5" %%i in ('findstr /b "root:" C:\msys2\etc\passwd') do set SID=%%i set SID=!SID:*,=!