Let the shell user install packages with pacman

The root user now owns the whole MSYS2 installation and can run pacman. The
login path the journey depends on - the sshd, the login shell zsh and cat - is
kept as a protected copy in C:\msys2\opt\kik that only SYSTEM and the
administrators may write, and the sshd runs as SYSTEM from there (the new
KiK sshd task; the msys2-sshd service is disabled). Replacing the user's own
/usr/bin/sshd therefore cannot touch the running server.

opt/kik is two levels below C:\msys2, so its runtime resolves the same
installation root and the login shell and the user's tools share one cygwin
runtime. A separate second installation does not work: two runtimes in one
process tree corrupt each other's file descriptors and pacman cannot lock its
database.

/etc/passwd and /etc/ssh are read-only to the user, /opt/kik is first on PATH
so cat README survives even if the user deletes their own /usr/bin/cat, and
the restore task rebuilds the protected copy and re-applies the permissions
every 30 minutes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zebreus 2026-10-25 21:37:55 +01:00
commit b228fec07e
3 changed files with 244 additions and 52 deletions

View file

@ -32,14 +32,32 @@ Anything thats not specified above (like the content of README for example) is N
As long as the user story above does not break, we do not guarantee the functionality of anything else on the server. However the cleanup task should still try to best-effort restore everything to a working condition.
## The cleanup task
## Packages
The scheduled task `KiK restore shell files` runs every 30 minutes and at every startup, as SYSTEM. The user cannot stop it. Cleanup has two jobs:
The root user owns the whole MSYS2 installation and can install packages with pacman. That means they can also
replace or delete the very programs the login needs. So the sshd, the login shell `zsh` and `cat` are kept as a
second, protected copy in `C:\msys2\opt\kik`, which only SYSTEM and the administrators may write. The sshd runs as
SYSTEM from that copy and the login shell is that copy's `zsh`, with `/opt/kik` first on the path, so the journey
keeps working no matter what the user does to their own `/usr/bin`. Replacing `/usr/bin/sshd` cannot touch the
running server, so the key on the website still never grants administrator access.
1. Act as a secondary safeguard to restore the essential files for the user journey above.
2. Revert any other mockery the user did that might affect the system.
The copy has to be the *same* MSYS2 installation, not a second one: `C:\msys2\opt\kik` is two levels below
`C:\msys2`, so its runtime resolves the same root and the login shell and the user's own tools share one cygwin
runtime. A separate installation does not work - two runtimes in one process tree corrupt each other's file
descriptors and pacman cannot even lock its database.
The second job should also be able to restore a new server to the expected state.
`/etc/passwd` (the login shell and home directory) and `/etc/ssh` (the host keys and config) are read-only to the
user as well; everything else in the installation is theirs.
## The cleanup tasks
Two scheduled tasks run as SYSTEM, both at startup, and the user cannot stop either:
- `KiK sshd` runs the sshd from `C:\msys2\opt\kik`. It replaces the installation's own `msys2-sshd` service, which
is disabled.
- `KiK restore shell files` runs every 30 minutes and rebuilds the protected copy from its trusted originals,
re-applies the permissions, and puts the home directory, the shell configuration, the website's key and the four
texts back. Its second job is to best-effort revert other mockery and bring a fresh 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

View file

@ -1,3 +1,7 @@
# The protected copies of zsh and cat (and the server itself) live here; keeping them first means the basic
# journey - the prompt and reading the four texts - survives even if the user breaks their own /usr/bin copies.
path=(/opt/kik $path)
# History shared between sessions.
HISTFILE=~/.zsh_history HISTSIZE=10000 SAVEHIST=10000
setopt share_history hist_ignore_dups hist_ignore_space interactive_comments auto_cd

View file

@ -1,10 +1,19 @@
# 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.
# Sets up the shell server so the website's root user can install packages with pacman, while the basic user
# journey - logging in with the key, the prompt, the home directory and the four text files - cannot be broken.
#
# The whole MSYS2 installation belongs to the root user: they may write everywhere and run pacman. The login path
# the journey depends on - the sshd, the login shell zsh and cat - is kept as a protected copy in C:\msys2\opt\kik
# that only SYSTEM and the administrators may write. The sshd runs as SYSTEM from that copy (the scheduled task
# "KiK sshd"), so replacing /usr/bin/sshd does not touch the running server. A second scheduled task,
# "KiK restore shell files", puts the home directory, the four texts and the protected copy back every 30 minutes.
#
# opt/kik works because its MSYS2 runtime resolves the same installation root (C:\msys2) as /usr/bin, so the login
# shell and the user's own tools share one cygwin runtime. A separate second installation cannot: two runtimes in
# one process tree break each other's file descriptors, and pacman then cannot even lock its database.
#
# 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
# Remove the task and the copies again with:
# Remove the tasks and the protected copy again (the installation goes back to a normal cygrunsrv sshd) with:
# powershell -ExecutionPolicy Bypass -File .\install_restore_task.ps1 -Uninstall
[CmdletBinding()]
param(
@ -13,24 +22,40 @@ param(
)
$ErrorActionPreference = 'Stop'
$msys = 'C:\msys2'
$kik = "$msys\opt\kik"
$installDir = 'C:\ProgramData\kik-restore'
$copiesDir = Join-Path $installDir 'files'
$taskName = 'KiK restore shell files'
$binDir = Join-Path $installDir 'bin'
$sshdBat = Join-Path $installDir 'run-sshd.bat'
$restoreTask = 'KiK restore shell files'
$sshdTask = 'KiK sshd'
$service = 'msys2-sshd'
# The programs the journey needs, kept out of the user's reach. sshd-session and sshd-auth are the per-connection
# helpers OpenSSH 10 re-executes; zsh is the login shell; cat reads the four texts.
$loginBinaries = "$msys\usr\bin\sshd.exe", "$msys\usr\lib\ssh\sshd-session.exe", "$msys\usr\lib\ssh\sshd-auth.exe",
"$msys\usr\bin\zsh.exe", "$msys\usr\bin\cat.exe"
# 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 home directory back into shape: the directory itself, the shell configuration that gives
# 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.
# Puts the shell server back into shape and keeps the protected login path in place. Runs as SYSTEM every 30 minutes
# and at startup; the root user can neither stop it nor change the copies it restores from.
param([string]$HomeDir = 'C:\msys2\home\root')
$msys = 'C:\msys2'
$kik = "$msys\opt\kik"
$log = Join-Path $PSScriptRoot 'restore.log'
$copies = Join-Path $PSScriptRoot 'files\home'
$binDir = Join-Path $PSScriptRoot 'bin'
# 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:*' }
$passwd = Get-Content "$msys\etc\passwd" -ErrorAction SilentlyContinue | Where-Object { $_ -like 'root:*' }
$sid = if ($passwd) { ($passwd -split ':')[4].Split(',')[-1] }
$system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18')
$admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
function Write-Log($message) {
"$(Get-Date -Format s) $message" | Out-File -FilePath $log -Append
}
@ -47,6 +72,23 @@ function Remove-Anything($path) {
}
}
# A path that only SYSTEM and the administrators own. $userRights is what the root user may do: 'None' (the ssh keys
# and config), 'Read' (the account database) or 'ReadAndExecute' (the login programs it has to run).
function Set-LockedAcl($path, $forDirectory, $userRights) {
$acl = Get-Acl -LiteralPath $path
$acl.SetAccessRuleProtection($true, $false)
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
$acl.SetOwner($admins)
$inherit = if ($forDirectory) { 'ContainerInherit,ObjectInherit' } else { 'None' }
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($system, 'FullControl', $inherit, 'None', 'Allow')))
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($admins, 'FullControl', $inherit, 'None', 'Allow')))
if ($userRights -ne 'None' -and $sid) {
$user = New-Object System.Security.Principal.SecurityIdentifier($sid)
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($user, $userRights, $inherit, 'None', 'Allow')))
}
Set-Acl -LiteralPath $path -AclObject $acl
}
# 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) {
@ -65,11 +107,9 @@ $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) {
function Set-HomeAcl($path, $forDirectory, $writable) {
if (-not $sid) { return }
try {
$system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18')
$admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
$user = New-Object System.Security.Principal.SecurityIdentifier($sid)
$acl = Get-Acl -LiteralPath $path
$acl.SetAccessRuleProtection($true, $false)
@ -77,16 +117,15 @@ function Set-ProtectedAcl($path, $forDirectory, $writable) {
$acl.SetOwner($admins)
$none = [System.Security.AccessControl.InheritanceFlags]::None
$children = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
$noPropagate = [System.Security.AccessControl.PropagationFlags]::None
$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 $system, $full, $inherit, 'None', 'Allow'))
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $admins, $full, $inherit, 'None', 'Allow'))
$userRights = if ($writable) { $some } else { $read }
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $userRights, $none, $noPropagate, 'Allow'))
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $user, $userRights, $none, 'None', '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'))
@ -101,8 +140,46 @@ function Set-ProtectedAcl($path, $forDirectory, $writable) {
# 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 $true
Get-ChildItem -LiteralPath 'C:\msys2\etc\skel' -Force | Copy-Item -Destination $path -Recurse -Force
Set-HomeAcl $path $true $true
Get-ChildItem -LiteralPath "$msys\etc\skel" -Force | Copy-Item -Destination $path -Recurse -Force
}
try {
# The protected login path: the copy the sshd and the login shell run from. The user cannot write it, so even
# after they replace or remove their own copies in /usr/bin the server and the journey keep working. The trusted
# originals are in bin\; anything missing from opt/kik is put back.
if (Test-Path -LiteralPath $binDir) {
if (-not (Test-Path -LiteralPath $kik)) { New-Item -ItemType Directory -Path $kik | Out-Null }
foreach ($file in Get-ChildItem -LiteralPath $binDir -File -Force) {
$target = Join-Path $kik $file.Name
if (-not (Test-Path -LiteralPath $target) -or (Get-Item -LiteralPath $target -Force).Length -ne $file.Length) {
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
Write-Log "restored opt/kik/$($file.Name)"
}
}
New-Item -ItemType Directory -Force -Path "$kik\dev\shm", "$kik\dev\mqueue" | Out-Null
Set-LockedAcl $kik $true 'ReadAndExecute'
# /dev is scratch: the user may write it so POSIX shared memory works, the rest of opt/kik stays read-only
if ($sid) { icacls "$kik\dev" /grant "*${sid}:(OI)(CI)M" | Out-Null }
}
# The account database and the ssh keys and config the SYSTEM server reads: the user may read passwd (programs
# map ids through it) but change none of it, and has no business in /etc/ssh at all.
$rootShell = '/opt/kik/zsh.exe'
$pw = "$msys\etc\passwd"
$lines = Get-Content $pw
if ($lines | Where-Object { $_ -match '^root:' -and $_ -notmatch ':/opt/kik/zsh\.exe$' }) {
($lines | ForEach-Object { if ($_ -match '^root:') { $_ -replace ':[^:]*$', ":$rootShell" } else { $_ } }) | Set-Content $pw -Encoding ascii
Write-Log 'set the root login shell to the protected zsh'
}
Set-LockedAcl $pw $false 'Read'
if (Test-Path "$msys\etc\ssh") { Set-LockedAcl "$msys\etc\ssh" $true 'None' }
# The whole installation otherwise belongs to the user, so pacman can write it. The home directory keeps its own
# protected permissions because they are set without inheritance.
if ($sid) { icacls $msys /grant "*${sid}:(OI)(CI)F" | Out-Null }
} catch {
Write-Log "the protected login path is not fully in place: $_"
}
if (-not (Test-Path -LiteralPath $HomeDir)) {
@ -113,7 +190,7 @@ if (-not (Test-Path -LiteralPath $HomeDir)) {
New-Home $HomeDir
Write-Log "replaced the link $HomeDir"
} else {
Set-ProtectedAcl $HomeDir $true $true
Set-HomeAcl $HomeDir $true $true
}
foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) {
@ -124,7 +201,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 $true
Set-HomeAcl $dir $true $true
if (Test-Path -LiteralPath $target) {
$item = Get-Item -LiteralPath $target -Force
if ($item.PSIsContainer -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
@ -140,7 +217,7 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) {
} else {
Copy-Item -LiteralPath $copy.FullName -Destination $target -Force
}
Set-ProtectedAcl $target $false ($readOnly -notcontains $copy.Name)
Set-HomeAcl $target $false ($readOnly -notcontains $copy.Name)
# the user may have made the file read-only for themselves, the four texts are theirs to change
(Get-Item -LiteralPath $target -Force).IsReadOnly = $false
Write-Log "restored $($copy.Name)"
@ -154,39 +231,90 @@ if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdent
throw 'Run this in an elevated PowerShell.'
}
$system = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18')
$admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
$passwd = Get-Content "$msys\etc\passwd" -ErrorAction SilentlyContinue | Where-Object { $_ -like 'root:*' }
if (-not $passwd) { throw "No root user in $msys\etc\passwd." }
$sid = ($passwd -split ':')[4].Split(',')[-1]
# A previous install may have locked opt/kik, the account database and the ssh keys. The administrators keep
# full control over them, so reset the permissions to inherited now and let this run write freely; the restore
# task locks everything again at the end.
$prevEA = $ErrorActionPreference; $ErrorActionPreference = 'SilentlyContinue'
foreach ($p in "$msys\opt\kik", "$msys\etc\ssh", "$msys\etc\passwd") {
if (Test-Path -LiteralPath $p) { icacls $p /reset /t /q | Out-Null }
}
Get-ChildItem "$msys\etc\ssh", "$msys\etc\passwd" -Force | ForEach-Object { try { $_.IsReadOnly = $false } catch {} }
$ErrorActionPreference = $prevEA
function Set-LockedAcl($path, $forDirectory, $userRights) {
$acl = Get-Acl -LiteralPath $path
$acl.SetAccessRuleProtection($true, $false)
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
$acl.SetOwner($admins)
$inherit = if ($forDirectory) { 'ContainerInherit,ObjectInherit' } else { 'None' }
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($system, 'FullControl', $inherit, 'None', 'Allow')))
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($admins, 'FullControl', $inherit, 'None', 'Allow')))
if ($userRights -ne 'None') {
$user = New-Object System.Security.Principal.SecurityIdentifier($sid)
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($user, $userRights, $inherit, 'None', 'Allow')))
}
Set-Acl -LiteralPath $path -AclObject $acl
}
# The DLLs an MSYS2 program loads, as Windows paths under C:\msys2.
function Get-Libraries($program) {
$paths = @()
$posix = ($program -replace '^C:\\msys2', '') -replace '\\', '/'
foreach ($line in & "$msys\usr\bin\ldd.exe" $posix 2>$null) {
foreach ($m in [regex]::Matches($line, '/usr/[^\s]+\.dll')) { $paths += $m.Value }
}
$paths | Select-Object -Unique | ForEach-Object { $msys + ($_ -replace '/', '\') }
}
if ($Uninstall) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
foreach ($t in $restoreTask, $sshdTask) { Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction SilentlyContinue }
if (Test-Path -LiteralPath $installDir) { Remove-Item -LiteralPath $installDir -Recurse -Force }
Write-Host "Removed the task and $installDir."
if (Test-Path -LiteralPath $kik) {
icacls $kik /reset /t /q 2>$null | Out-Null
Remove-Item -LiteralPath $kik -Recurse -Force -ErrorAction SilentlyContinue
}
# the installation's own cygrunsrv sshd takes over again
$pw = "$msys\etc\passwd"
(Get-Content $pw | ForEach-Object { if ($_ -match '^root:') { $_ -replace ':[^:]*$', ':/usr/bin/zsh' } else { $_ } }) | Set-Content $pw -Encoding ascii
icacls $pw /reset | Out-Null
if (Test-Path "$msys\etc\ssh") { icacls "$msys\etc\ssh" /reset /t /q 2>$null | Out-Null }
Set-Service $service -StartupType Automatic -ErrorAction SilentlyContinue
Start-Service $service -ErrorAction SilentlyContinue
Write-Host "Removed the tasks and $installDir; the $service service serves ssh again."
return
}
$resources = Join-Path $PSScriptRoot '..\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
if ($prepared -and $prepared.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$installDir is a link, refusing to install." }
New-Item -ItemType Directory -Force -Path $copiesDir | Out-Null
New-Item -ItemType Directory -Force -Path $copiesDir, $binDir | Out-Null
# Only SYSTEM and the administrators may read or write the copies and the script.
# Only SYSTEM and the administrators may read or write the copies, the trusted login programs and the scripts.
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
foreach ($sid in 'S-1-5-18', 'S-1-5-32-544') {
$identity = New-Object System.Security.Principal.SecurityIdentifier $sid
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList $identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
foreach ($owner in $system, $admins) {
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($owner, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
}
$acl.SetOwner((New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32-544'))
$acl.SetOwner($admins)
Set-Acl -LiteralPath $installDir -AclObject $acl
# Files the root user prepared are removed, the copies are written fresh and inherit the restricted permissions.
foreach ($item in Get-ChildItem -LiteralPath $copiesDir -Recurse -Force) {
foreach ($item in Get-ChildItem -LiteralPath $installDir -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 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'
@ -199,23 +327,65 @@ 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
# The trusted login programs: the sshd, its helpers, the login shell and cat, with every DLL they load. The restore
# task puts these into opt/kik, which the user cannot write.
Remove-Item "$binDir\*" -Force -ErrorAction SilentlyContinue
$files = @{}
foreach ($b in $loginBinaries) {
if (-not (Test-Path -LiteralPath $b)) { throw "$b does not exist." }
$files[$b] = $true
foreach ($lib in Get-Libraries $b) { $files[$lib] = $true }
}
foreach ($f in $files.Keys) { Copy-Item -LiteralPath $f -Destination $binDir -Force }
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\restore.ps1`" -HomeDir `"$HomeDir`""
$triggers = @(
# The sshd reads its configuration and finds its helpers in opt/kik; nothing here depends on the user's /usr.
$sshdConfig = "$msys\etc\ssh\sshd_config"
Set-Content -LiteralPath $sshdConfig -Encoding ascii -Value @(
'# The website logs in as root, the deployers as Administrator. Both come in over the websocket bridge.',
'Port 22',
'AllowUsers root Administrator',
'# Files are protected by Windows ACLs; under MSYS2''s noacl mounts the POSIX modes sshd checks are fake.',
'StrictModes no',
'Subsystem sftp internal-sftp',
'# The server and its per-connection helpers are the protected copies, not the ones the user can replace.',
'SshdSessionPath /opt/kik/sshd-session.exe',
'SshdAuthPath /opt/kik/sshd-auth.exe'
)
# The sshd runs as SYSTEM from the protected copy, with opt/kik first on the path so its own DLLs and the session's
# programs (zsh, cat) are the protected ones. The log stays in the install directory, out of the user's reach.
Set-Content -LiteralPath $sshdBat -Encoding ascii -Value @(
'@echo off',
'set PATH=C:\msys2\opt\kik;C:\msys2\usr\bin;%PATH%',
'C:\msys2\opt\kik\sshd.exe -D -e > C:\ProgramData\kik-restore\sshd.log 2>&1'
)
# The installation's own cygrunsrv sshd service would fight for port 22; the scheduled task owns the sshd instead.
Set-Service $service -StartupType Disabled -ErrorAction SilentlyContinue
Stop-Service $service -Force -ErrorAction SilentlyContinue
$atSystem = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$sshdAction = New-ScheduledTaskAction -Execute $sshdBat
$sshdSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Seconds 0) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName $sshdTask -Action $sshdAction -Trigger (New-ScheduledTaskTrigger -AtStartup) -Principal $atSystem -Settings $sshdSettings -Force | Out-Null
$restoreAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\restore.ps1`" -HomeDir `"$HomeDir`""
$restoreTriggers = @(
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30) -RepetitionDuration (New-TimeSpan -Days 3650)
New-ScheduledTaskTrigger -AtStartup
)
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $triggers -Principal $principal -Settings $settings -Force | Out-Null
$restoreSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName $restoreTask -Action $restoreAction -Trigger $restoreTriggers -Principal $atSystem -Settings $restoreSettings -Force | Out-Null
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 8 -ErrorAction SilentlyContinue
icacls $installDir
# The restore task builds opt/kik and sets every permission; run it before starting the sshd that depends on it.
Start-ScheduledTask -TaskName $restoreTask
Start-Sleep -Seconds 6
$waited = 0
while ((Get-ScheduledTask -TaskName $restoreTask).State -eq 'Running' -and $waited -lt 60) { Start-Sleep 2; $waited += 2 }
Start-ScheduledTask -TaskName $sshdTask
Start-Sleep -Seconds 4
Write-Host "The sshd runs as SYSTEM from C:\msys2\opt\kik (task '$sshdTask'); '$restoreTask' keeps it and the home directory in shape."
Get-Content (Join-Path $installDir 'restore.log') -Tail 10 -ErrorAction SilentlyContinue
Get-CimInstance Win32_Process -Filter "Name='sshd.exe'" | ForEach-Object { $_.CommandLine }