From 88e5ab264e036749fde839eb77d82784b6a62713 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Fri, 21 Aug 2026 09:12:56 -0400 Subject: [PATCH] Harden IIS deploy script auth, smoke test, and 32-bit ACE OLEDB handling - scp/ssh now force a fallback to password auth so a missing/rejected key fails with a real prompt instead of an opaque "Permission denied (publickey)". - Smoke test now checks each path against its expected status (/404 is expected to 404) instead of treating any non-throwing response as a pass, and surfaces the actual failure reason. - The remote IIS server has the 64-bit Access Database Engine, unlike the local dev machine, so its app pool stays 64-bit and migrations run through the plain (64-bit) cscript.exe instead of SysWOW64. Co-Authored-By: Claude Sonnet 5 --- scripts/deploy-iis-remote-apply.ps1 | 6 ++- scripts/deploy-iis.ps1 | 57 +++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/scripts/deploy-iis-remote-apply.ps1 b/scripts/deploy-iis-remote-apply.ps1 index 604e4a8..69be89d 100644 --- a/scripts/deploy-iis-remote-apply.ps1 +++ b/scripts/deploy-iis-remote-apply.ps1 @@ -65,11 +65,15 @@ icacls $dataDir /grant ("IIS AppPool\" + $AppPool + ":(OI)(CI)(M)") /T | Out-Nul Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $publicDir Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name applicationPool -Value $AppPool +# This server has the 64-bit Access Database Engine (ACE OLEDB) installed, unlike the local +# dev machine, which needs the 32-bit one - so the app pool stays 64-bit here. +Set-ItemProperty ('IIS:\AppPools\' + $AppPool) -Name enable32BitAppOnWin64 -Value $false + if($RunMigrations){ Write-Host 'Running pending migrations' Push-Location $RemoteDir try { - & C:\Windows\SysWOW64\cscript.exe //nologo scripts\runMigrations.vbs up + & cscript.exe //nologo scripts\runMigrations.vbs up if($LASTEXITCODE -ne 0){ throw 'runMigrations.vbs up failed - see output above' } } finally { Pop-Location diff --git a/scripts/deploy-iis.ps1 b/scripts/deploy-iis.ps1 index e654f26..5c009ac 100644 --- a/scripts/deploy-iis.ps1 +++ b/scripts/deploy-iis.ps1 @@ -76,12 +76,20 @@ $remoteZip = 'C:\Windows\Temp\' + $zipName $remoteApplyScript = Join-Path $PSScriptRoot 'deploy-iis-remote-apply.ps1' $remoteApplyDest = 'C:\Windows\Temp\deploy-iis-remote-apply.ps1' +# Force a fall-through to password auth: if pubkey auth isn't set up (or an agent offers a +# key the server doesn't accept), plain ssh/scp can otherwise fail outright with +# "Permission denied (publickey)" instead of ever prompting for a password. +$AuthOpts = @( + '-o', 'PreferredAuthentications=publickey,keyboard-interactive,password', + '-o', 'NumberOfPasswordPrompts=3' +) + Write-Host "Copying release to $RemoteTarget" -& $ScpExe -P $RemotePort $localZip "${RemoteTarget}:$remoteZip" -if($LASTEXITCODE -ne 0){ throw 'scp of release zip failed' } +& $ScpExe -P $RemotePort @AuthOpts $localZip "${RemoteTarget}:$remoteZip" +if($LASTEXITCODE -ne 0){ throw 'scp of release zip failed - see scp output above for the actual reason' } -& $ScpExe -P $RemotePort $remoteApplyScript "${RemoteTarget}:$remoteApplyDest" -if($LASTEXITCODE -ne 0){ throw 'scp of remote-apply script failed' } +& $ScpExe -P $RemotePort @AuthOpts $remoteApplyScript "${RemoteTarget}:$remoteApplyDest" +if($LASTEXITCODE -ne 0){ throw 'scp of remote-apply script failed - see scp output above for the actual reason' } # --- 3. Apply on the remote host --- $remoteCommandParts = @( @@ -96,8 +104,8 @@ $remoteCommandParts = @( if($RunMigrations){ $remoteCommandParts += '-RunMigrations' } Write-Host "Applying release on $RemoteTarget" -& $SshExe -p $RemotePort $RemoteTarget ($remoteCommandParts -join ' ') -if($LASTEXITCODE -ne 0){ throw 'remote apply failed' } +& $SshExe -p $RemotePort @AuthOpts $RemoteTarget ($remoteCommandParts -join ' ') +if($LASTEXITCODE -ne 0){ throw 'remote apply failed - see ssh output above for the actual reason' } # --- 4. Local cleanup --- Remove-Item $localZip -ErrorAction SilentlyContinue @@ -105,11 +113,38 @@ Remove-Item -Recurse -Force $outDir -ErrorAction SilentlyContinue # --- 5. Smoke test --- Write-Host 'Smoke testing...' -$paths = @('/', '/request-order', '/404') -foreach($path in $paths){ - $url = $BaseUrl.TrimEnd('/') + $path - $response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 30 - Write-Host ("OK " + $path + ' -> ' + $response.StatusCode) +# /404 is the app's own not-found route - a 404 there is correct, not a failure. +$checks = @( + @{ Path = '/'; Expect = 200 }, + @{ Path = '/request-order'; Expect = 200 }, + @{ Path = '/404'; Expect = 404 } +) +$failed = $false +foreach($check in $checks){ + $url = $BaseUrl.TrimEnd('/') + $check.Path + try { + $response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 30 + $status = [int]$response.StatusCode + } catch { + if($_.Exception.Response){ + $status = [int]$_.Exception.Response.StatusCode + } else { + Write-Host ("FAIL " + $check.Path + ' -> request failed: ' + $_.Exception.Message) + $failed = $true + continue + } + } + + if($status -eq $check.Expect){ + Write-Host ("OK " + $check.Path + ' -> ' + $status) + } else { + Write-Host ("FAIL " + $check.Path + ' -> ' + $status + ' (expected ' + $check.Expect + ')') + $failed = $true + } +} + +if($failed){ + throw 'Smoke test failed - see above' } Write-Host 'Deploy complete.'