diff --git a/server/README.md b/server/README.md index 63d668d..b329c36 100644 --- a/server/README.md +++ b/server/README.md @@ -65,7 +65,9 @@ undoes that. - `home/` - the files that belong in the home directory of the user: `~/.zshrc`, `~/.zshenv` and the keys in `~/.ssh/authorized_keys` - the one the website logs in with and the ones of the people who deploy. This is where they are edited. -- `resources/` - the copies the cleanup restores from: the four text files and the files of `home/`. +- `admin/` - the same for the home directory of the Administrator: the keys of the people who deploy. The key of the + website is not among them and never gets administrator rights. +- `resources/` - the copies the cleanup restores from: the files of `home/` and `admin/` and the four text files. - `scripts/make_files.py` - builds the resources. - `scripts/deploy.sh` - deploys the cleanup to the server and installs it. - `scripts/install_restore_task.ps1` - installs the scheduled task. @@ -80,10 +82,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 logs in with the personal key of the deployer, -`~/.ssh/id_ed25519` by default and `-i` for another one; 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. +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 that the key of the website is the first one in +`home/.ssh/authorized_keys`. `--uninstall` removes the task again. The installer can also be run by hand, in an elevated PowerShell on the server: diff --git a/server/admin/.ssh/authorized_keys b/server/admin/.ssh/authorized_keys new file mode 100644 index 0000000..c6a6df9 --- /dev/null +++ b/server/admin/.ssh/authorized_keys @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIBTHzLm8QMhHIo7kFAvtAFnqpspeR3L3gM8kLoG1137 lennart@erms diff --git a/server/resources/admin/.ssh/authorized_keys b/server/resources/admin/.ssh/authorized_keys new file mode 100644 index 0000000..c6a6df9 --- /dev/null +++ b/server/resources/admin/.ssh/authorized_keys @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIBTHzLm8QMhHIo7kFAvtAFnqpspeR3L3gM8kLoG1137 lennart@erms diff --git a/server/resources/.ssh/authorized_keys b/server/resources/home/.ssh/authorized_keys similarity index 100% rename from server/resources/.ssh/authorized_keys rename to server/resources/home/.ssh/authorized_keys diff --git a/server/resources/.zshenv b/server/resources/home/.zshenv similarity index 100% rename from server/resources/.zshenv rename to server/resources/home/.zshenv diff --git a/server/resources/.zshrc b/server/resources/home/.zshrc similarity index 100% rename from server/resources/.zshrc rename to server/resources/home/.zshrc diff --git a/server/resources/CODE_OF_CONDUCT b/server/resources/home/CODE_OF_CONDUCT similarity index 100% rename from server/resources/CODE_OF_CONDUCT rename to server/resources/home/CODE_OF_CONDUCT diff --git a/server/resources/CODE_OF_CONDUCT.en b/server/resources/home/CODE_OF_CONDUCT.en similarity index 100% rename from server/resources/CODE_OF_CONDUCT.en rename to server/resources/home/CODE_OF_CONDUCT.en diff --git a/server/resources/README b/server/resources/home/README similarity index 100% rename from server/resources/README rename to server/resources/home/README diff --git a/server/resources/README.en b/server/resources/home/README.en similarity index 100% rename from server/resources/README.en rename to server/resources/home/README.en diff --git a/server/scripts/deploy.sh b/server/scripts/deploy.sh index 03bdaf6..09fae47 100755 --- a/server/scripts/deploy.sh +++ b/server/scripts/deploy.sh @@ -6,13 +6,13 @@ # 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, by default ~/.ssh/id_ed25519. The key of the -# website never gets administrator rights: it only logs in as the root user. +# 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 USER=Administrator -KEYFILE="$HOME/.ssh/id_ed25519" +KEYFILE='' UNINSTALL=0 while [ $# -gt 0 ]; do @@ -30,11 +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 [ ! -f "$KEYFILE" ]; then - echo "no key file for the login as $USER: $KEYFILE" >&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) @@ -47,19 +42,15 @@ if [ "$actual" != "$expected" ]; then exit 1 fi -# 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}') -if ! awk 'NF {print $1" "$2}' "$server/resources/.ssh/authorized_keys" | grep -qxF "$public"; then - echo "the key in server/resources/.ssh/authorized_keys is not the one the website uses" >&2 +# The site logs in with the first key in the authorized_keys of the user on the box, so it has to be the right one. +sitekey=$(ssh-keygen -lf "$server/resources/home/.ssh/authorized_keys" | awk 'NR==1 {print $2}') +if [ "$sitekey" != 'SHA256:KUEv4d4+75h41l0OGTWCA2FbtEOQZ047ujkeh+kkFW0' ]; then + echo "the key of the website is not the first one in server/resources/home/.ssh/authorized_keys" >&2 exit 1 fi -options=(-i "$KEYFILE" -o "UserKnownHostsFile=$known" -o StrictHostKeyChecking=yes) +options=(-o "UserKnownHostsFile=$known" -o StrictHostKeyChecking=yes) +if [ -n "$KEYFILE" ]; then options+=(-i "$KEYFILE"); fi ssh=(ssh "${options[@]}" "$USER@$HOST") scp=(scp "${options[@]}") @@ -68,13 +59,9 @@ if [ "$UNINSTALL" = 1 ]; then exit fi -# The installer looks for the resources next to itself, so both directories go to the server. -"${ssh[@]}" "if not exist $remote mkdir $remote" +# The installer looks for the resources next to itself, so both directories go to the server. What was there +# before is removed, so that files that are gone from the repository do not stay on the box. +"${ssh[@]}" "if exist $remote rmdir /s /q $remote" +"${ssh[@]}" "mkdir $remote" "${scp[@]}" -r "$server/scripts" "$server/resources" "$USER@$HOST:$remotePath/" "${ssh[@]}" "powershell -NoProfile -ExecutionPolicy Bypass -File $remote\\scripts\\install_restore_task.ps1" - -# The Administrator logs in with the key of the deployer, so that it is there after every deployment. -keyline=$(ssh-keygen -y -f "$KEYFILE") -blob=$(echo "$keyline" | awk 'NF {print $2}') -"${ssh[@]}" "if not exist C:\\Users\\Administrator\\.ssh mkdir C:\\Users\\Administrator\\.ssh" -"${ssh[@]}" "findstr /c:\"$blob\" C:\\Users\\Administrator\\.ssh\\authorized_keys >nul 2>&1 || echo $keyline >> C:\\Users\\Administrator\\.ssh\\authorized_keys" diff --git a/server/scripts/install_restore_task.ps1 b/server/scripts/install_restore_task.ps1 index 003b27f..e5f106c 100644 --- a/server/scripts/install_restore_task.ps1 +++ b/server/scripts/install_restore_task.ps1 @@ -25,7 +25,7 @@ $restoreScript = @' # 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' +$copies = Join-Path $PSScriptRoot 'files\home' # 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:*' } @@ -51,10 +51,10 @@ function Remove-Anything($path) { # 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] + $blobs = $required | ForEach-Object { ($_ -split '\s+')[1] } $keys = @() if (Test-Path -LiteralPath $target) { - $keys = @(Get-Content -LiteralPath $target | Where-Object { $_.Trim() -and ($_ -split '\s+')[1] -ne $blob }) + $keys = @(Get-Content -LiteralPath $target | Where-Object { $_.Trim() -and $blobs -notcontains ($_ -split '\s+')[1] }) } [IO.File]::WriteAllText($target, (($keys + $required) -join "`n") + "`n") } @@ -127,7 +127,7 @@ if ($Uninstall) { } $resources = Join-Path $PSScriptRoot '..\resources' -if (-not (Get-ChildItem -LiteralPath $resources -File -Force)) { throw "No files in $resources." } +if (-not (Get-ChildItem -LiteralPath $resources -Recurse -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 @@ -152,6 +152,16 @@ 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 key of the people who deploy, which is in the resources. The keys that are +# already there stay. +$adminKeys = 'C:\Users\Administrator\.ssh\authorized_keys' +$required = @(Get-Content -LiteralPath (Join-Path $resources 'admin\.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/make_files.py b/server/scripts/make_files.py index e9bd1cd..425d54c 100644 --- a/server/scripts/make_files.py +++ b/server/scripts/make_files.py @@ -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 -# files of ../home, which are copied as they are. +# Builds the files for the home directories of the shell server in ../resources: the home of the root user with +# README, README.en, CODE_OF_CONDUCT and CODE_OF_CONDUCT.en as the website's pages in terminal text with escape +# sequences, and the files of ../home and ../admin, which are copied as they are. # Usage: python3 server/scripts/make_files.py import pathlib import shutil @@ -332,16 +332,18 @@ COC_EN = { } out = pathlib.Path(__file__).parent.parent / 'resources' -out.mkdir(exist_ok=True) +(out / 'home').mkdir(parents=True, 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 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) + (out / 'home' / name).write_text(text, encoding='utf-8') +# The files of the home directories are not generated, they are copied from ../home and ../admin, where they are +# edited. +for source in 'home', 'admin': + root = pathlib.Path(__file__).parent.parent / source + for path in root.rglob('*'): + target = out / source / path.relative_to(root) + if path.is_dir(): + target.mkdir(parents=True, exist_ok=True) + else: + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, target) diff --git a/server/scripts/restore_ssh_access.cmd b/server/scripts/restore_ssh_access.cmd index 01e61b6..0ab3f71 100644 --- a/server/scripts/restore_ssh_access.cmd +++ b/server/scripts/restore_ssh_access.cmd @@ -1,15 +1,19 @@ @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. Run it as Administrator on the box, for example over RDP. The cleanup job puts the key -REM back by itself from then on. +REM home of the root user, and the key of the deployer in the home of the Administrator. Run it as Administrator on +REM the box, for example over RDP. The cleanup job puts the key of the website back by itself from then on. setlocal EnableDelayedExpansion set KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDweEH8bhZAHVaAedhTVDb2j/oMEn3vm97xCwCw9LC9 root@nowhere.wirs.ing cygwin-2222 set OWN=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIBTHzLm8QMhHIo7kFAvtAFnqpspeR3L3gM8kLoG1137 lennart@erms +set BLOB=AAAAC3NzaC1lZDI1NTE5AAAAIIBTHzLm8QMhHIo7kFAvtAFnqpspeR3L3gM8kLoG1137 if not exist C:\msys2\home\root\.ssh mkdir C:\msys2\home\root\.ssh > C:\msys2\home\root\.ssh\authorized_keys echo %KEY% echo %OWN% >> C:\msys2\home\root\.ssh\authorized_keys +if not exist C:\Users\Administrator\.ssh mkdir C:\Users\Administrator\.ssh +findstr /c:"%BLOB%" C:\Users\Administrator\.ssh\authorized_keys >nul 2>&1 || echo %OWN% >> C:\Users\Administrator\.ssh\authorized_keys + 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:*,=!