<# .SYNOPSIS Firecradle Agent - one-command installer for Windows. .DESCRIPTION Published to the CDN at, and only at: https://cdn.shadowcradle.com/download/fc-agent/win/stable/latest/install.ps1 This file is the published file, byte for byte. `python3 sandbox.py publish-stage` copies it; nothing generates it. If you are reading it from the CDN and want to know it is ours, read it in git instead: build/installer/cdn/install.ps1 in git.takelan.com/shadowcradle/firecradle-agent. WHAT THE ONE-LINER TRUSTS, PLAINLY irm https://cdn.shadowcradle.com/.../install.ps1 | iex is offered because it is what people will do anyway, and this is a truthful account of it. Piping to `iex` from an elevated prompt means: * you run whatever the CDN serves at that moment, as Administrator, without reading it. TLS proves you reached cdn.shadowcradle.com. It does not prove that what cdn.shadowcradle.com serves today is what was reviewed; * this script then verifies the installer's SHA-256 against a digest it fetches from THE SAME BUCKET. That catches a corrupted or truncated download. It catches nothing about someone who can write to the bucket, because they would write both; * this script verifies NO release signature, and the installer .exe is NOT SIGNED (no Authenticode certificate). A release may carry a detached signature beside its versioned copy (.sig); this script neither fetches nor checks it, so nothing in this path survives a bad bucket. Check it by hand from a checkout that pins the key: tools/sign_release.py verify --pinned. It says whether a release was signed; this script cannot know, because it reads only VERSION. THE TWO-STEP, WHICH IS THE ONE TO USE IF YOU CARE: irm https://cdn.shadowcradle.com/.../install.ps1 -OutFile install.ps1 irm https://cdn.shadowcradle.com/.../install.ps1.sha256 Get-FileHash install.ps1 -Algorithm SHA256 # compare with both notepad install.ps1 # read it .\install.ps1 These are not equivalent and this script will not pretend they are. WHAT THE DIGEST BESIDE THIS SCRIPT IS WORTH, AND WHAT IT IS NOT This script is published with an `install.ps1.sha256` beside it, like every other object in the release. THAT DIGEST DOES NOTHING FOR THE ONE-LINER. A pipe into `iex` consumes the bytes as they arrive and never has them again, so there is no second copy to hash and nothing to compare against -- by the time you could check, the script has already run elevated. The digest is for the two-step above, where you fetch the file, keep it, and check it before running it, and for a mirror that stores this script and re-checks it later. It is not what makes the one-liner safe, because nothing does. And it is served from the same bucket by the same host as this script, so like every other digest in this release it is evidence about corruption and not about origin. Comparing it with `build/installer/cdn/install.ps1` in git is the check that is worth something. THERE IS NO ENROLMENT SECRET IN HERE, AND THERE WILL NOT BE This script installs the agent. It does not enrol it. Sign-in is the RFC 8628 device authorization grant: the tray shows a code, a person signs in in their own browser, and the device gets its own revocable credential. A provisioning key is an ORGANISATION-WIDE credential - holding one is enough to enrol new devices claiming any principal. Putting one in a script published on a CDN and pasted into consoles would publish it to every desktop that runs the command, and into PowerShell's own transcript and history. This script REFUSES such a parameter rather than accepting it quietly. Managed enrolment has its own channel: Intune, an RMM or a GPO passes it to setup.exe as /ENROLLMENTKEY=, which writes a file under an ACL granting only SYSTEM and Administrators. See build/installer/INSTALLER-NOTES.md, "Managed enrolment". .PARAMETER Channel The CDN release train to install from. Default: stable. One of old-stable, stable, release-candidate, rolling, dev. THESE ARE NOT THE INSTALLED AGENT'S UPDATE CHANNELS. They are path segments on cdn.shadowcradle.com. The agent has its own, separate, two-value update-channel setting (stable | beta) which it sends to the cloud; "beta" there is not a directory here and has never been published to. Both vocabularies contain "stable" and it means the same release train in both. .PARAMETER Version Install this exact version instead of whatever the channel says is current. .PARAMETER Yes Do not ask for confirmation. .PARAMETER Force Reinstall even when this version is already installed. .PARAMETER KeepDownload Leave the downloaded installer in place for inspection. #> [CmdletBinding()] param( [ValidateSet('old-stable', 'stable', 'release-candidate', 'rolling', 'dev')] [string]$Channel = 'stable', [string]$Version = '', [switch]$Yes, [switch]$Force, [switch]$KeepDownload, # Anything else. Present ONLY so a provisioning key passed here is refused # loudly instead of being silently ignored by the parameter binder, which # would leave the person believing the device was enrolled. [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Rest ) $ErrorActionPreference = 'Stop' function Stop-Install { param([int]$Code, [string]$Message) Write-Host '' Write-Host 'Firecradle Agent was NOT installed.' -ForegroundColor Red Write-Host $Message exit $Code } if ($Rest) { $secretish = $Rest | Where-Object { $_ -match '(?i)enrol|provision|orgkey|org-key|token|secret|password|apikey|api-key' } if ($secretish) { Stop-Install 2 @' That option is not accepted, on purpose. A provisioning key is an organisation-wide credential: whoever holds it can enrol new devices claiming any principal. Passing one to a script that is published on a CDN and pasted into consoles puts it into PowerShell's history and transcript on every machine it is run on. To install one device, run this script with no key and sign in from the Firecradle tray. It uses a browser device-code login, so the device gets its own credential your administrator can revoke on its own. To deploy a fleet, pass the key to the installer through the channel your management tool already uses: setup.exe /VERYSILENT /ENROLLMENTKEY=... /ENROLLMENTCODE=... which writes it under an ACL granting only SYSTEM and Administrators. See build/installer/INSTALLER-NOTES.md, "Managed enrolment". '@ } Stop-Install 2 ("Unknown option: {0}. Run Get-Help on this script to see what is accepted." -f ($Rest -join ' ')) } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Stop-Install 3 @' This installer needs an elevated PowerShell: it registers a Windows service. Open PowerShell with "Run as administrator" and run it again. It does not need to stay elevated afterwards - the Firecradle tray runs unelevated on purpose, and a UAC prompt during normal use would be a bug. '@ } if ($env:PROCESSOR_ARCHITECTURE -notin @('AMD64', 'x86')) { Stop-Install 4 ("There is no Firecradle Agent build for {0}. The published Windows installer is x64." -f $env:PROCESSOR_ARCHITECTURE) } $base = if ($env:FIRECRADLE_CDN_BASE) { $env:FIRECRADLE_CDN_BASE } else { 'https://cdn.shadowcradle.com' } # The product is the first path segment and the PLATFORM SLUG is the second. # The slug is `win`, NOT `windows` -- the build platform is called `windows` # everywhere in the repository and `win` is what a URL calls it, and # build/publish.py's PLATFORM_SLUGS is the only place the two meet. This script # is the Windows one, so its slug is fixed here rather than detected: the # platform is which file you are reading. $download = "$base/download/fc-agent/win/$Channel" # TLS 1.2 explicitly: Windows PowerShell 5.1 still defaults to a protocol set # the CDN refuses, and the failure it produces ("The request was aborted: Could # not create SSL/TLS secure channel") reads as a certificate problem. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } function Get-Text { param([string]$Uri) try { return (Invoke-WebRequest -Uri $Uri -UseBasicParsing -MaximumRedirection 5).Content } catch { return $null } } if (-not $Version) { $text = Get-Text "$download/latest/VERSION" if ($null -eq $text) { Stop-Install 5 ("Could not ask {0} which version is current. Check this machine's network and proxy settings, then run it again." -f $base) } $Version = $text.Trim() } if (-not $Version) { Stop-Install 5 "$download/latest/VERSION was empty, so there is nothing to install. That is a publishing fault, not a fault on this machine - please report it." } if ($Version -notmatch '^[0-9][0-9A-Za-z.]*([-+][0-9A-Za-z.]+)*$') { Stop-Install 5 ("'{0}' is not a version number. Refusing to build a download URL out of it." -f $Version) } $file = "firecradle-agent-setup-$Version.exe" $url = "$download/$Version/$file" # Idempotence, read out of the registry rather than by running anything. The # AppId is the one in build/installer/windows/firecradle-agent.iss; Inno # appends _is1. $uninstallKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{9F2B6E3A-2C41-4E7A-9E2B-3C7D1A4F5B60}_is1' $installed = $null try { $installed = (Get-ItemProperty -Path $uninstallKey -ErrorAction Stop).DisplayVersion } catch { } if ($installed -eq $Version -and -not $Force) { Write-Host "Firecradle Agent $Version is already installed." Write-Host 'Nothing to do. Pass -Force to reinstall it anyway.' exit 0 } $replacing = if ($installed) { " (replacing $installed)" } else { '' } Write-Host '' Write-Host 'Firecradle Agent - about to install' Write-Host '' Write-Host " version $Version$replacing" Write-Host " channel $Channel" Write-Host " download $url" Write-Host " checksum $url.sha256" Write-Host ' installs the Firecradle Agent service (SYSTEM) and the tray app' Write-Host ' runs the installer silently, with no reboot' Write-Host '' Write-Host ' This script does NOT verify a release signature, and the installer' Write-Host ' .exe is NOT SIGNED (Authenticode). The SHA-256 above comes from the' Write-Host ' same bucket as the download, so it proves nothing about who produced' Write-Host ' the file - only that what you received arrived intact.' Write-Host '' Write-Host ' Nothing is enrolled by this script. After it finishes, open the' Write-Host ' Firecradle tray and sign in - the browser device-code login gives' Write-Host ' this device its own revocable credential.' Write-Host '' if (-not $Yes) { # Read from the host, not from the pipeline. Under `irm ... | iex` the # pipeline is the script's own text; consuming it would run half a program. # If there is no interactive host, refuse and say what to pass, rather than # reading silence as consent. if ([Environment]::UserInteractive -and $Host.UI.RawUI) { $answer = Read-Host 'Continue? [y/N]' if ($answer -notmatch '^(y|yes)$') { Write-Host 'Nothing was changed.' exit 0 } } else { Stop-Install 6 @" This is running without an interactive console to ask on (a scheduled task, or an automation runner). Nothing was changed. Re-run it with -Yes if you have read the plan above: & ([scriptblock]::Create((irm $download/latest/install.ps1))) -Yes "@ } } $work = Join-Path ([IO.Path]::GetTempPath()) ("firecradle-" + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $work -Force | Out-Null $installer = Join-Path $work $file try { Write-Host "==> Downloading $file" try { Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing } catch { Stop-Install 7 ("The download failed: {0}`n{1}`nIf you passed -Version, check that this version was published." -f $url, $_.Exception.Message) } Write-Host '==> Fetching the published SHA-256' $sidecar = Get-Text "$url.sha256" if ($null -eq $sidecar) { Stop-Install 7 ("The download succeeded and its checksum did not: {0}.sha256`nRefusing to install an artefact that cannot be checked." -f $url) } $expected = ($sidecar -split '\s+')[0] if ($expected -notmatch '^[0-9a-fA-F]{64}$') { Stop-Install 8 ("{0}.sha256 did not contain a SHA-256 digest. Refusing to install." -f $url) } $actual = (Get-FileHash -Path $installer -Algorithm SHA256).Hash if ($actual.ToLowerInvariant() -ne $expected.ToLowerInvariant()) { Stop-Install 8 @" CHECKSUM MISMATCH - nothing was installed. expected $($expected.ToLowerInvariant()) got $($actual.ToLowerInvariant()) The download does not match what the CDN says it published. Usually that is a truncated or cached-and-corrupted download, so trying again from a different network is worth doing. If it happens repeatedly, stop and report it. "@ } Write-Host " OK $($actual.ToLowerInvariant())" Write-Host '==> Installing' # /VERYSILENT /NORESTART: the installer's own UI would be a second thing to # answer after the plan above was already agreed to, and a reboot is never # taken on someone's behalf. $process = Start-Process -FilePath $installer ` -ArgumentList '/VERYSILENT', '/NORESTART', '/SUPPRESSMSGBOXES' ` -Wait -PassThru if ($process.ExitCode -ne 0) { Stop-Install 10 @" The installer ran and exited with code $($process.ExitCode). The system may be partly installed. Re-running this command is safe and is the usual repair. Inno Setup's exit codes are listed at https://jrsoftware.org/ishelp/ - code 5 is "the user cancelled", and 2 is "an unhandled error during setup". "@ } } finally { if ($KeepDownload) { Write-Host "Downloaded files kept in $work" } else { Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue } } Write-Host '' Write-Host 'Firecradle Agent is installed and the service is running.' Write-Host '' Write-Host 'Next: open Firecradle from the Start menu and sign in. It will show a' Write-Host 'code and open your browser; that is the device sign-in, and it is what' Write-Host 'gives this device its own credential. Nothing was enrolled by this script.' Write-Host ''