1173 lines
42 KiB
PowerShell
1173 lines
42 KiB
PowerShell
#Requires -RunAsAdministrator
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$ApproveFilesystemTrustResetAfterReboot
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$Target = "C:\MinerOpsAgent"
|
|
$ConfigPath = Join-Path $Target "agent-config.json"
|
|
$PendingConfigPath = Join-Path $Target "agent-config.json.pending"
|
|
$RecoveryConfigPath = Join-Path $Target "agent-config.recovery.json"
|
|
$EnrollmentPath = Join-Path $PSScriptRoot "enrollment.json"
|
|
$Installer = Join-Path $PSScriptRoot "install.ps1"
|
|
|
|
$RequiredFiles = @(
|
|
"install.ps1",
|
|
"MinerOpsAgentService.ps1",
|
|
"MinerOpsAgentTray.ps1",
|
|
"MinerOpsAgentGUI.ps1",
|
|
"MinerOpsApi3EnrichmentPoller.ps1",
|
|
"MinerOpsPoolPoller.ps1",
|
|
"MinerOpsPoolPollerTask.ps1",
|
|
"EnrichOneMiner.ps1",
|
|
"MinerOpsControl.ps1",
|
|
"VERSION",
|
|
"enrollment.json"
|
|
)
|
|
|
|
function Get-MinerOpsEnrollmentDirectorySids {
|
|
return @(@(
|
|
"S-1-5-18",
|
|
"S-1-5-32-544"
|
|
) | Select-Object -Unique)
|
|
}
|
|
|
|
function New-MinerOpsEnrollmentCredentialAcl {
|
|
$acl = New-Object System.Security.AccessControl.FileSecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.SetOwner((New-Object `
|
|
System.Security.Principal.SecurityIdentifier("S-1-5-32-544")))
|
|
foreach ($sidValue in (Get-MinerOpsEnrollmentDirectorySids)) {
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
(New-Object System.Security.Principal.SecurityIdentifier($sidValue)),
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($rule)
|
|
}
|
|
return $acl
|
|
}
|
|
|
|
function Test-MinerOpsEnrollmentCredentialAcl {
|
|
param([Parameter(Mandatory = $true)]$Acl)
|
|
|
|
if (-not $Acl.AreAccessRulesProtected) { return $false }
|
|
try {
|
|
if ($Acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value -ne "S-1-5-32-544") { return $false }
|
|
}
|
|
catch { return $false }
|
|
$expected = @{}
|
|
foreach ($sid in (Get-MinerOpsEnrollmentDirectorySids)) {
|
|
$expected[$sid] = $false
|
|
}
|
|
$rules = @($Acl.GetAccessRules(
|
|
$true, $false, [System.Security.Principal.SecurityIdentifier]
|
|
))
|
|
if ($rules.Count -ne $expected.Count) { return $false }
|
|
foreach ($rule in $rules) {
|
|
$sid = $rule.IdentityReference.Value
|
|
if (
|
|
-not $expected.ContainsKey($sid) -or
|
|
$rule.IsInherited -or
|
|
$rule.AccessControlType -ne
|
|
[System.Security.AccessControl.AccessControlType]::Allow -or
|
|
$rule.FileSystemRights -ne
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl
|
|
) { return $false }
|
|
$expected[$sid] = $true
|
|
}
|
|
return -not ($expected.Values -contains $false)
|
|
}
|
|
|
|
function Set-MinerOpsEnrollmentStreamAcl {
|
|
param([Parameter(Mandatory)]$Stream, [Parameter(Mandatory)]$Acl)
|
|
if ($Stream.PSObject.Methods.Name -contains "SetAccessControl") {
|
|
$Stream.SetAccessControl($Acl)
|
|
}
|
|
else {
|
|
[System.IO.FileSystemAclExtensions]::SetAccessControl($Stream, $Acl)
|
|
}
|
|
}
|
|
|
|
function New-MinerOpsEnrollmentProtectedFileCreateNewStream {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)]
|
|
[System.Security.AccessControl.FileSecurity]$Acl
|
|
)
|
|
|
|
$rights = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::WriteData -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Synchronize
|
|
)
|
|
return [IO.FileStream]::new(
|
|
$Path,
|
|
[IO.FileMode]::CreateNew,
|
|
$rights,
|
|
[IO.FileShare]::None,
|
|
4096,
|
|
[IO.FileOptions]::None,
|
|
$Acl
|
|
)
|
|
}
|
|
|
|
function Open-MinerOpsEnrollmentFileStreamWithRights {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)]
|
|
[System.Security.AccessControl.FileSystemRights]$Rights
|
|
)
|
|
|
|
return [IO.FileStream]::new(
|
|
$Path,
|
|
[IO.FileMode]::Open,
|
|
$Rights,
|
|
[IO.FileShare]::None,
|
|
4096,
|
|
[IO.FileOptions]::None
|
|
)
|
|
}
|
|
|
|
function Get-MinerOpsEnrollmentStreamAcl {
|
|
param([Parameter(Mandatory)]$Stream)
|
|
if ($Stream.PSObject.Methods.Name -contains "GetAccessControl") {
|
|
return $Stream.GetAccessControl()
|
|
}
|
|
return [System.IO.FileSystemAclExtensions]::GetAccessControl($Stream)
|
|
}
|
|
|
|
function Assert-MinerOpsEnrollmentSingleLinkStream {
|
|
param(
|
|
[Parameter(Mandatory)]$Stream,
|
|
[Parameter(Mandatory)][string]$Description
|
|
)
|
|
|
|
if (-not ("MinerOpsFileIdentityNative" -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using Microsoft.Win32.SafeHandles;
|
|
public static class MinerOpsFileIdentityNative
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct ByHandleFileInformation
|
|
{
|
|
public uint FileAttributes;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
|
|
public uint VolumeSerialNumber;
|
|
public uint FileSizeHigh;
|
|
public uint FileSizeLow;
|
|
public uint NumberOfLinks;
|
|
public uint FileIndexHigh;
|
|
public uint FileIndexLow;
|
|
}
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool GetFileInformationByHandle(
|
|
SafeFileHandle file, out ByHandleFileInformation information);
|
|
public static uint GetNumberOfLinks(SafeFileHandle file)
|
|
{
|
|
ByHandleFileInformation information;
|
|
if (!GetFileInformationByHandle(file, out information))
|
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
return information.NumberOfLinks;
|
|
}
|
|
}
|
|
'@
|
|
}
|
|
if ([MinerOpsFileIdentityNative]::GetNumberOfLinks(
|
|
$Stream.SafeFileHandle
|
|
) -ne 1) {
|
|
throw "$Description must have exactly one filesystem link."
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsEnrollmentTrustBlockerPaths {
|
|
param([Parameter(Mandatory)][string]$Path)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$parent = [IO.Directory]::GetParent($fullPath)
|
|
if ($null -eq $parent) { throw "Agent directory cannot be a filesystem root." }
|
|
$pattern = ([IO.Path]::GetFileName($fullPath)) +
|
|
".minerops-filesystem-trust.blocked-*.state"
|
|
return @(Get-ChildItem -LiteralPath $parent.FullName `
|
|
-Filter $pattern -File -Force -ErrorAction Stop)
|
|
}
|
|
|
|
function ConvertFrom-MinerOpsEnrollmentKernelBootEventRecord {
|
|
param([Parameter(Mandatory)]$EventRecord)
|
|
|
|
if (
|
|
$null -eq $EventRecord -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "ProviderName" -or
|
|
$EventRecord.ProviderName -isnot [string] -or
|
|
[string]$EventRecord.ProviderName -cne
|
|
"Microsoft-Windows-Kernel-Boot" -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "Id" -or
|
|
$EventRecord.Id -isnot [int] -or
|
|
[int]$EventRecord.Id -ne 27 -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "RecordId" -or
|
|
$EventRecord.RecordId -isnot [long] -or
|
|
[long]$EventRecord.RecordId -le 0 -or
|
|
$EventRecord.PSObject.Properties.Name -cnotcontains "Version" -or
|
|
$EventRecord.Version -isnot [byte] -or
|
|
[byte]$EventRecord.Version -notin @([byte]0, [byte]1) -or
|
|
$EventRecord.PSObject.Methods.Name -cnotcontains "ToXml"
|
|
) {
|
|
throw "Kernel-Boot event metadata is invalid or unsupported."
|
|
}
|
|
|
|
$xmlText = $EventRecord.ToXml()
|
|
if ($xmlText -isnot [string] -or
|
|
[string]::IsNullOrWhiteSpace([string]$xmlText)) {
|
|
throw "Kernel-Boot event XML is missing."
|
|
}
|
|
try { [xml]$eventXml = [string]$xmlText }
|
|
catch { throw "Kernel-Boot event XML is malformed." }
|
|
|
|
$eventNamespace = "http://schemas.microsoft.com/win/2004/08/events/event"
|
|
if (
|
|
$null -eq $eventXml.DocumentElement -or
|
|
$eventXml.DocumentElement.LocalName -cne "Event" -or
|
|
$eventXml.DocumentElement.NamespaceURI -cne $eventNamespace
|
|
) {
|
|
throw "Kernel-Boot event XML root is invalid."
|
|
}
|
|
$namespaceManager = [Xml.XmlNamespaceManager]::new($eventXml.NameTable)
|
|
$namespaceManager.AddNamespace("evt", $eventNamespace)
|
|
$eventNodes = @($eventXml.SelectNodes("/evt:Event", $namespaceManager))
|
|
$systemNodes = @($eventXml.SelectNodes(
|
|
"/evt:Event/evt:System", $namespaceManager
|
|
))
|
|
$eventDataNodes = @($eventXml.SelectNodes(
|
|
"/evt:Event/evt:EventData", $namespaceManager
|
|
))
|
|
$bootTypeNodes = @($eventXml.SelectNodes(
|
|
"/evt:Event/evt:EventData/evt:Data[@Name='BootType']",
|
|
$namespaceManager
|
|
))
|
|
if (
|
|
$eventNodes.Count -ne 1 -or
|
|
$systemNodes.Count -ne 1 -or
|
|
$eventDataNodes.Count -ne 1 -or
|
|
$bootTypeNodes.Count -ne 1 -or
|
|
$bootTypeNodes[0].ChildNodes.Count -ne 1 -or
|
|
$bootTypeNodes[0].FirstChild.NodeType -ne [Xml.XmlNodeType]::Text
|
|
) {
|
|
throw "Kernel-Boot event XML schema is invalid or ambiguous."
|
|
}
|
|
|
|
$bootTypeText = [string]$bootTypeNodes[0].InnerText
|
|
if ($bootTypeText -cnotmatch '^(0|[1-9][0-9]*)$') {
|
|
throw "Kernel-Boot event BootType is not canonical."
|
|
}
|
|
$bootType = [UInt32]0
|
|
if (-not [UInt32]::TryParse(
|
|
$bootTypeText,
|
|
[Globalization.NumberStyles]::None,
|
|
[Globalization.CultureInfo]::InvariantCulture,
|
|
[ref]$bootType
|
|
)) {
|
|
throw "Kernel-Boot event BootType is invalid."
|
|
}
|
|
return [pscustomobject]@{
|
|
RecordId = [long]$EventRecord.RecordId
|
|
Version = [byte]$EventRecord.Version
|
|
BootType = [UInt32]$bootType
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsEnrollmentBootIdentifier {
|
|
try {
|
|
$events = @(Get-WinEvent -FilterHashtable @{
|
|
LogName = "System"
|
|
ProviderName = "Microsoft-Windows-Kernel-Boot"
|
|
Id = 27
|
|
} -ErrorAction Stop)
|
|
$fullBootRecordIds = @($events | ForEach-Object {
|
|
$record = ConvertFrom-MinerOpsEnrollmentKernelBootEventRecord `
|
|
-EventRecord $_
|
|
if ($record.BootType -eq 0) { [long]$record.RecordId }
|
|
} | Sort-Object -Descending)
|
|
if ($fullBootRecordIds.Count -eq 0) {
|
|
throw "Windows full-kernel-boot event is missing."
|
|
}
|
|
for ($index = 1; $index -lt $fullBootRecordIds.Count; $index++) {
|
|
if ([long]$fullBootRecordIds[$index - 1] -eq
|
|
[long]$fullBootRecordIds[$index]) {
|
|
throw "Windows full-kernel-boot event is ambiguous."
|
|
}
|
|
}
|
|
return "windows-full-boot-event-v1:{0:x16}" -f `
|
|
[UInt64]$fullBootRecordIds[0]
|
|
}
|
|
catch { throw "Windows boot identity could not be verified." }
|
|
}
|
|
|
|
function ConvertFrom-MinerOpsEnrollmentBootIdentifier {
|
|
param([Parameter(Mandatory)]$Value)
|
|
|
|
if ($Value -isnot [string] -or
|
|
$Value -cnotmatch '^windows-full-boot-event-v1:[0-9a-f]{16}$') {
|
|
throw "Enrollment filesystem trust blocker boot identity is invalid."
|
|
}
|
|
$parsed = [UInt64]0
|
|
if (-not [UInt64]::TryParse(
|
|
$Value.Substring(27),
|
|
[Globalization.NumberStyles]::AllowHexSpecifier,
|
|
[Globalization.CultureInfo]::InvariantCulture,
|
|
[ref]$parsed
|
|
) -or $parsed -eq 0) {
|
|
throw "Enrollment filesystem trust blocker boot identity is invalid."
|
|
}
|
|
return "windows-full-boot-event-v1:{0:x16}" -f $parsed
|
|
}
|
|
|
|
function New-MinerOpsEnrollmentTrustBlocker {
|
|
param([Parameter(Mandatory)][string]$TargetRoot)
|
|
|
|
$fullTarget = [IO.Path]::GetFullPath($TargetRoot).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$parent = [IO.Directory]::GetParent($fullTarget).FullName
|
|
$path = Join-Path $parent (
|
|
(([IO.Path]::GetFileName($fullTarget)) +
|
|
".minerops-filesystem-trust.blocked-{0}.state") -f
|
|
[guid]::NewGuid().ToString("N")
|
|
)
|
|
$stream = $null
|
|
try {
|
|
# The marker is outside the contaminated directory so a handle opened
|
|
# inside that directory before repair cannot remove the durable block.
|
|
$stream = New-MinerOpsEnrollmentProtectedFileCreateNewStream `
|
|
-Path $path -Acl (New-MinerOpsEnrollmentCredentialAcl)
|
|
if (-not (Test-MinerOpsEnrollmentCredentialAcl `
|
|
-Acl (Get-MinerOpsEnrollmentStreamAcl -Stream $stream))) {
|
|
throw "Enrollment filesystem trust blocker ACL could not be verified."
|
|
}
|
|
Assert-MinerOpsEnrollmentSingleLinkStream -Stream $stream `
|
|
-Description "Enrollment filesystem trust blocker"
|
|
$record = [ordered]@{
|
|
version = 2
|
|
state = "blocked"
|
|
reason = "existing-agent-directory-was-untrusted"
|
|
target_path = [IO.Path]::GetFullPath($TargetRoot)
|
|
detected_boot_id = Get-MinerOpsEnrollmentBootIdentifier
|
|
reset_boot_id = $null
|
|
original_task_running = $null
|
|
created_at = [DateTime]::UtcNow.ToString("o")
|
|
}
|
|
$bytes = (New-Object Text.UTF8Encoding($false)).GetBytes(
|
|
(($record | ConvertTo-Json -Depth 10) + [Environment]::NewLine)
|
|
)
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
|
$stream.Flush($true)
|
|
}
|
|
finally {
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
return $path
|
|
}
|
|
|
|
function Open-MinerOpsEnrollmentCredentialFileCreateNew {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$ExpectedParent
|
|
)
|
|
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $ExpectedParent)
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$actualParent = [IO.Directory]::GetParent($fullPath).FullName.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$expected = [IO.Path]::GetFullPath($ExpectedParent).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
if (-not $actualParent.Equals(
|
|
$expected, [StringComparison]::OrdinalIgnoreCase
|
|
)) { throw "Enrollment credential path is outside the Agent directory." }
|
|
|
|
$stream = $null
|
|
$createdByThisCall = $false
|
|
try {
|
|
$stream = New-MinerOpsEnrollmentProtectedFileCreateNewStream `
|
|
-Path $fullPath -Acl (New-MinerOpsEnrollmentCredentialAcl)
|
|
$createdByThisCall = $true
|
|
if (-not (Test-MinerOpsEnrollmentCredentialAcl `
|
|
-Acl (Get-MinerOpsEnrollmentStreamAcl -Stream $stream))) {
|
|
throw "Enrollment credential file ACL could not be verified."
|
|
}
|
|
Assert-MinerOpsEnrollmentSingleLinkStream -Stream $stream `
|
|
-Description "Enrollment credential file"
|
|
return $stream
|
|
}
|
|
catch {
|
|
if ($null -ne $stream) {
|
|
$stream.Dispose()
|
|
$stream = $null
|
|
}
|
|
if ($createdByThisCall -and
|
|
(Test-Path -LiteralPath $fullPath -PathType Leaf)) {
|
|
Remove-Item -LiteralPath $fullPath -Force `
|
|
-ErrorAction SilentlyContinue
|
|
}
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Write-MinerOpsEnrollmentCredentialJson {
|
|
param(
|
|
[Parameter(Mandatory)]$Stream,
|
|
[Parameter(Mandatory)][string]$Json
|
|
)
|
|
|
|
Assert-MinerOpsEnrollmentSingleLinkStream -Stream $Stream `
|
|
-Description "Enrollment credential file"
|
|
$bytes = (New-Object Text.UTF8Encoding($false)).GetBytes(
|
|
$Json + [Environment]::NewLine
|
|
)
|
|
$Stream.Write($bytes, 0, $bytes.Length)
|
|
$Stream.Flush($true)
|
|
Assert-MinerOpsEnrollmentSingleLinkStream -Stream $Stream `
|
|
-Description "Enrollment credential file"
|
|
}
|
|
|
|
function Assert-MinerOpsEnrollmentCredentialFile {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$ExpectedParent
|
|
)
|
|
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $ExpectedParent)
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$actualParent = [IO.Directory]::GetParent($fullPath).FullName.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$expected = [IO.Path]::GetFullPath($ExpectedParent).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
if (-not $actualParent.Equals(
|
|
$expected, [StringComparison]::OrdinalIgnoreCase
|
|
)) { throw "Enrollment credential path is outside the Agent directory." }
|
|
$item = Get-Item -LiteralPath $fullPath -Force -ErrorAction Stop
|
|
if (
|
|
$item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
|
($item.PSObject.Properties.Name -contains "LinkType" -and
|
|
[string]$item.LinkType)
|
|
) { throw "Enrollment credential path is not a regular single-link file." }
|
|
|
|
$stream = $null
|
|
try {
|
|
$stream = Open-MinerOpsEnrollmentFileStreamWithRights `
|
|
-Path $fullPath `
|
|
-Rights ([System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Synchronize
|
|
))
|
|
Assert-MinerOpsEnrollmentSingleLinkStream -Stream $stream `
|
|
-Description "Enrollment credential file"
|
|
if (-not (Test-MinerOpsEnrollmentCredentialAcl `
|
|
-Acl (Get-MinerOpsEnrollmentStreamAcl -Stream $stream))) {
|
|
throw "Enrollment credential file protection could not be verified."
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function New-MinerOpsEnrollmentDirectoryAcl {
|
|
$acl = New-Object System.Security.AccessControl.DirectorySecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$administrators = New-Object System.Security.Principal.SecurityIdentifier(
|
|
"S-1-5-32-544"
|
|
)
|
|
$acl.SetOwner($administrators)
|
|
foreach ($sidValue in (Get-MinerOpsEnrollmentDirectorySids)) {
|
|
$identity = New-Object System.Security.Principal.SecurityIdentifier(
|
|
$sidValue
|
|
)
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
$identity,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($rule)
|
|
}
|
|
$users = New-Object System.Security.Principal.SecurityIdentifier(
|
|
"S-1-5-32-545"
|
|
)
|
|
$readRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
$users,
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute,
|
|
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($readRule)
|
|
return $acl
|
|
}
|
|
|
|
function Assert-MinerOpsEnrollmentNoReparsePath {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$root = [IO.Path]::GetPathRoot($fullPath)
|
|
if (-not $root) { throw "Agent directory must use an absolute path." }
|
|
if (-not $fullPath.Equals(
|
|
$root, [StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
$fullPath = $fullPath.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
$current = $root
|
|
$rootItem = Get-Item -LiteralPath $current -Force -ErrorAction Stop
|
|
if (
|
|
-not $rootItem.PSIsContainer -or
|
|
($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
) {
|
|
throw "Agent directory path root is a reparse point or non-directory component."
|
|
}
|
|
$relative = $fullPath.Substring($root.Length)
|
|
foreach ($segment in @($relative.Split(
|
|
@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar),
|
|
[StringSplitOptions]::RemoveEmptyEntries
|
|
))) {
|
|
$current = Join-Path $current $segment
|
|
$item = Get-Item -LiteralPath $current -Force -ErrorAction Stop
|
|
if (
|
|
-not $item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
) {
|
|
throw "Agent directory path contains a reparse point or non-directory component."
|
|
}
|
|
}
|
|
return $fullPath
|
|
}
|
|
|
|
function Assert-MinerOpsEnrollmentTrustedAncestors {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$root = [IO.Path]::GetPathRoot($fullPath)
|
|
if (-not $fullPath.Equals(
|
|
$root, [StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
$fullPath = $fullPath.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
$parentInfo = [IO.Directory]::GetParent($fullPath)
|
|
if ($null -eq $parentInfo) { throw "Agent directory cannot be a filesystem root." }
|
|
$parent = $parentInfo.FullName
|
|
$root = [IO.Path]::GetPathRoot($parent)
|
|
if (-not $parent.Equals(
|
|
$root, [StringComparison]::OrdinalIgnoreCase
|
|
)) {
|
|
$parent = $parent.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
$current = $root
|
|
$components = [System.Collections.Generic.List[string]]::new()
|
|
$components.Add($current)
|
|
foreach ($segment in @($parent.Substring($root.Length).Split(
|
|
@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar),
|
|
[StringSplitOptions]::RemoveEmptyEntries
|
|
))) {
|
|
$current = Join-Path $current $segment
|
|
$components.Add($current)
|
|
}
|
|
$trusted = @{}
|
|
foreach ($sid in @(
|
|
"S-1-5-18", "S-1-5-32-544",
|
|
"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464",
|
|
[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
)) { $trusted[$sid] = $true }
|
|
$dangerous = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::Delete -bor
|
|
[System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
)
|
|
foreach ($component in $components) {
|
|
$item = Get-Item -LiteralPath $component -Force -ErrorAction Stop
|
|
if (-not $item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
|
throw "Agent directory ancestor is not trusted."
|
|
}
|
|
$acl = Get-Acl -LiteralPath $component -ErrorAction Stop
|
|
try {
|
|
$owner = $acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value
|
|
}
|
|
catch { throw "Agent directory ancestor owner could not be verified." }
|
|
if (-not $trusted.ContainsKey($owner)) {
|
|
throw "Agent directory ancestor has an untrusted owner."
|
|
}
|
|
foreach ($rule in @($acl.GetAccessRules(
|
|
$true, $true, [System.Security.Principal.SecurityIdentifier]
|
|
))) {
|
|
if (
|
|
$rule.AccessControlType -eq
|
|
[System.Security.AccessControl.AccessControlType]::Allow -and
|
|
-not $trusted.ContainsKey($rule.IdentityReference.Value) -and
|
|
($rule.PropagationFlags -band
|
|
[System.Security.AccessControl.PropagationFlags]::InheritOnly) -eq 0 -and
|
|
($rule.FileSystemRights -band $dangerous) -ne 0
|
|
) {
|
|
throw "Agent directory ancestor permits untrusted namespace replacement."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Open-MinerOpsEnrollmentDirectoryLock {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
if (-not ("MinerOpsDirectoryLockNative" -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
using Microsoft.Win32.SafeHandles;
|
|
public static class MinerOpsDirectoryLockNative
|
|
{
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
public static extern SafeFileHandle CreateFile(string name, uint access,
|
|
uint share, IntPtr security, uint creation, uint flags,
|
|
IntPtr templateFile);
|
|
}
|
|
'@
|
|
}
|
|
$handle = [MinerOpsDirectoryLockNative]::CreateFile(
|
|
[IO.Path]::GetFullPath($Path), 0, 3, [IntPtr]::Zero, 3,
|
|
0x02200000, [IntPtr]::Zero
|
|
)
|
|
if ($handle.IsInvalid) {
|
|
$code = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
$handle.Dispose()
|
|
throw (New-Object ComponentModel.Win32Exception(
|
|
$code, "Agent directory could not be locked against replacement."
|
|
))
|
|
}
|
|
return $handle
|
|
}
|
|
|
|
function Test-MinerOpsEnrollmentDirectoryAcl {
|
|
param([Parameter(Mandatory = $true)]$Acl)
|
|
|
|
if (-not $Acl.AreAccessRulesProtected) { return $false }
|
|
$trusted = @{}
|
|
foreach ($sid in (Get-MinerOpsEnrollmentDirectorySids)) {
|
|
$trusted[$sid] = $false
|
|
}
|
|
$usersRead = $false
|
|
try {
|
|
$owner = $Acl.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier]
|
|
).Value
|
|
}
|
|
catch { return $false }
|
|
if (-not $trusted.ContainsKey($owner)) { return $false }
|
|
$writeLike = [System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::Write -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Delete -bor
|
|
[System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
)
|
|
foreach ($rule in @($Acl.GetAccessRules(
|
|
$true, $false, [System.Security.Principal.SecurityIdentifier]
|
|
))) {
|
|
$sid = $rule.IdentityReference.Value
|
|
if (
|
|
$rule.IsInherited -or
|
|
$rule.AccessControlType -ne
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
) { return $false }
|
|
if ($trusted.ContainsKey($sid)) {
|
|
if (($rule.FileSystemRights -band
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl) -ne
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl) {
|
|
return $false
|
|
}
|
|
$trusted[$sid] = $true
|
|
}
|
|
elseif ($sid -eq "S-1-5-32-545") {
|
|
if (
|
|
($rule.FileSystemRights -band $writeLike) -ne 0 -or
|
|
($rule.FileSystemRights -band
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute) -ne
|
|
[System.Security.AccessControl.FileSystemRights]::ReadAndExecute
|
|
) { return $false }
|
|
$usersRead = $true
|
|
}
|
|
else { return $false }
|
|
}
|
|
return $usersRead -and -not ($trusted.Values -contains $false)
|
|
}
|
|
|
|
function Assert-MinerOpsEnrollmentDirectory {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
$fullPath = Assert-MinerOpsEnrollmentNoReparsePath -Path $Path
|
|
[void](Assert-MinerOpsEnrollmentTrustedAncestors -Path $fullPath)
|
|
if (-not (Test-MinerOpsEnrollmentDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $fullPath -ErrorAction Stop))) {
|
|
throw "Agent directory owner or DACL is not trusted."
|
|
}
|
|
return $fullPath
|
|
}
|
|
|
|
function Protect-MinerOpsEnrollmentExistingConfig {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Path,
|
|
[Parameter(Mandatory = $true)][string]$ExpectedParent
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) { return }
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $ExpectedParent)
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
$parent = [IO.Directory]::GetParent($fullPath).FullName.TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$expected = [IO.Path]::GetFullPath($ExpectedParent).TrimEnd(
|
|
[IO.Path]::DirectorySeparatorChar,
|
|
[IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
$item = Get-Item -LiteralPath $fullPath -Force -ErrorAction Stop
|
|
if (
|
|
-not $parent.Equals($expected, [StringComparison]::OrdinalIgnoreCase) -or
|
|
$item.PSIsContainer -or
|
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
) {
|
|
throw "Existing Agent config is not a regular file in the Agent directory."
|
|
}
|
|
$stream = $null
|
|
try {
|
|
$stream = Open-MinerOpsEnrollmentFileStreamWithRights `
|
|
-Path $fullPath `
|
|
-Rights ([System.Security.AccessControl.FileSystemRights](
|
|
[System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Synchronize
|
|
))
|
|
Assert-MinerOpsEnrollmentSingleLinkStream -Stream $stream `
|
|
-Description "Existing Agent config"
|
|
Set-MinerOpsEnrollmentStreamAcl -Stream $stream `
|
|
-Acl (New-MinerOpsEnrollmentCredentialAcl)
|
|
if (-not (Test-MinerOpsEnrollmentCredentialAcl `
|
|
-Acl (Get-MinerOpsEnrollmentStreamAcl -Stream $stream))) {
|
|
throw "Existing Agent config ACL could not be verified."
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Initialize-MinerOpsEnrollmentDirectory {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Path,
|
|
[switch]$ApproveFilesystemTrustResetAfterReboot
|
|
)
|
|
|
|
$requestedPath = [IO.Path]::GetFullPath($Path)
|
|
if (-not (Test-Path -LiteralPath $requestedPath)) {
|
|
$parent = [IO.Directory]::GetParent($requestedPath).FullName
|
|
[void](Assert-MinerOpsEnrollmentNoReparsePath -Path $parent)
|
|
[void](Assert-MinerOpsEnrollmentTrustedAncestors -Path $requestedPath)
|
|
$staging = Join-Path $parent (
|
|
".minerops-agent-enroll-{0}" -f [guid]::NewGuid().ToString("N")
|
|
)
|
|
$stagingCreated = $false
|
|
try {
|
|
(New-Object IO.DirectoryInfo($staging)).Create(
|
|
(New-MinerOpsEnrollmentDirectoryAcl)
|
|
)
|
|
$stagingCreated = $true
|
|
if (-not (Test-MinerOpsEnrollmentDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $staging -ErrorAction Stop))) {
|
|
throw "Staged Agent directory DACL could not be verified."
|
|
}
|
|
[void](Assert-MinerOpsEnrollmentTrustedAncestors -Path $requestedPath)
|
|
[IO.Directory]::Move($staging, $requestedPath)
|
|
$stagingCreated = $false
|
|
}
|
|
finally {
|
|
if ($stagingCreated -and (Test-Path -LiteralPath $staging)) {
|
|
Remove-Item -LiteralPath $staging -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
$fullPath = Assert-MinerOpsEnrollmentNoReparsePath -Path $requestedPath
|
|
[void](Assert-MinerOpsEnrollmentTrustedAncestors -Path $fullPath)
|
|
$wasTrusted = Test-MinerOpsEnrollmentDirectoryAcl `
|
|
-Acl (Get-Acl -LiteralPath $fullPath -ErrorAction Stop)
|
|
if (-not $wasTrusted) {
|
|
$directoryLock = $null
|
|
$trustBlockerPath = New-MinerOpsEnrollmentTrustBlocker `
|
|
-TargetRoot $fullPath
|
|
try {
|
|
$directoryLock = Open-MinerOpsEnrollmentDirectoryLock -Path $fullPath
|
|
[void](Assert-MinerOpsEnrollmentNoReparsePath -Path $fullPath)
|
|
[void](Assert-MinerOpsEnrollmentTrustedAncestors -Path $fullPath)
|
|
Set-Acl -LiteralPath $fullPath `
|
|
-AclObject (New-MinerOpsEnrollmentDirectoryAcl)
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $fullPath)
|
|
}
|
|
finally {
|
|
if ($null -ne $directoryLock) { $directoryLock.Dispose() }
|
|
}
|
|
throw @"
|
|
The existing Agent directory was untrusted. It was hardened for containment,
|
|
but enrollment is blocked for this and every ordinary rerun. Reboot and follow
|
|
the documented protected-recreation trust-reset procedure. No token was
|
|
redeemed and no permanent credential was generated or written.
|
|
"@
|
|
}
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $fullPath)
|
|
$trustBlockers = @(
|
|
Get-MinerOpsEnrollmentTrustBlockerPaths -Path $fullPath
|
|
)
|
|
if ($trustBlockers.Count -gt 0) {
|
|
throw @"
|
|
Enrollment cannot clear an existing filesystem trust blocker, including with
|
|
-ApproveFilesystemTrustResetAfterReboot. Preserve evidence and use the
|
|
documented protected-recreation incident procedure. Only the authoritative
|
|
existing-Agent credential-transition parent has in-script blocker-deletion
|
|
authority after its complete convergence checks.
|
|
"@
|
|
}
|
|
Protect-MinerOpsEnrollmentExistingConfig `
|
|
-Path (Join-Path $fullPath "agent-config.json") `
|
|
-ExpectedParent $fullPath
|
|
if (@(Get-ChildItem -LiteralPath $fullPath `
|
|
-Filter "minerops-credential-transition.blocked*.state" `
|
|
-Force -ErrorAction Stop).Count -gt 0) {
|
|
throw "Credential transition is blocked pending operator review."
|
|
}
|
|
return $fullPath
|
|
}
|
|
|
|
function ConvertFrom-MinerOpsSecureString {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[Security.SecureString] $SecureValue
|
|
)
|
|
|
|
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR(
|
|
$SecureValue
|
|
)
|
|
|
|
try {
|
|
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
|
|
}
|
|
finally {
|
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
|
|
}
|
|
}
|
|
|
|
# Validate the complete package before modifying the Agent host or consuming
|
|
# the single-use enrollment token.
|
|
foreach ($name in $RequiredFiles) {
|
|
$source = Join-Path $PSScriptRoot $name
|
|
|
|
if (!(Test-Path -LiteralPath $source -PathType Leaf)) {
|
|
throw "Enrollment package is missing $name"
|
|
}
|
|
}
|
|
|
|
$enrollment = Get-Content -LiteralPath $EnrollmentPath -Raw |
|
|
ConvertFrom-Json
|
|
|
|
if ([string]$enrollment.workflow -ne "new_agent") {
|
|
throw @"
|
|
This package is not authorized for a new Agent enrollment.
|
|
Use the bootstrap script named by INSTALL.txt.
|
|
"@
|
|
}
|
|
|
|
$HubUrl = ([string] $enrollment.hub_url).TrimEnd("/")
|
|
$EnrollmentToken = ([string] $enrollment.token).Trim()
|
|
|
|
$parsedHub = $null
|
|
|
|
if (
|
|
-not [Uri]::TryCreate(
|
|
$HubUrl,
|
|
[UriKind]::Absolute,
|
|
[ref] $parsedHub
|
|
) -or
|
|
$parsedHub.Scheme -notin @("http", "https")
|
|
) {
|
|
throw "enrollment.json contains an invalid MinerOps Hub URL."
|
|
}
|
|
|
|
if (
|
|
$EnrollmentToken.Length -lt 32 -or
|
|
$EnrollmentToken.Length -gt 256
|
|
) {
|
|
throw "enrollment.json contains an invalid enrollment token."
|
|
}
|
|
|
|
$Target = Initialize-MinerOpsEnrollmentDirectory `
|
|
-Path $Target `
|
|
-ApproveFilesystemTrustResetAfterReboot:$ApproveFilesystemTrustResetAfterReboot
|
|
$ConfigPath = Join-Path $Target "agent-config.json"
|
|
$PendingConfigPath = Join-Path $Target "agent-config.json.pending"
|
|
$RecoveryConfigPath = Join-Path $Target "agent-config.recovery.json"
|
|
|
|
if (Test-Path -LiteralPath $ConfigPath) {
|
|
throw @"
|
|
C:\MinerOpsAgent\agent-config.json already exists.
|
|
This enrollment bootstrapper is for a new Agent identity and will not overwrite
|
|
an existing identity. Use the normal MinerOps upgrade procedure instead.
|
|
"@
|
|
}
|
|
|
|
foreach ($recoveryPath in @($PendingConfigPath, $RecoveryConfigPath)) {
|
|
if (Test-Path -LiteralPath $recoveryPath) {
|
|
throw @"
|
|
A previous enrollment recovery file exists at:
|
|
$recoveryPath
|
|
|
|
Do not create or redeem another enrollment until that recovery state has been
|
|
reviewed.
|
|
"@
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "MinerOps Windows Agent enrollment"
|
|
Write-Host "Hub: $HubUrl"
|
|
Write-Host ""
|
|
Write-Host "API3 credentials stay only on this Windows Agent."
|
|
Write-Host "Leave the account blank if API3 features are not being configured yet."
|
|
Write-Host ""
|
|
|
|
$Api3Account = (Read-Host "WhatsMiner API3 account").Trim()
|
|
$Api3PasswordPlain = ""
|
|
|
|
if ($Api3Account) {
|
|
$Api3PasswordSecure = Read-Host `
|
|
"WhatsMiner API3 password" `
|
|
-AsSecureString
|
|
|
|
$Api3PasswordPlain = ConvertFrom-MinerOpsSecureString `
|
|
-SecureValue $Api3PasswordSecure
|
|
}
|
|
|
|
$Redeemed = $false
|
|
$ConfigPersisted = $false
|
|
$pendingStream = $null
|
|
$recoveryStream = $null
|
|
$PendingCreatedByThisInvocation = $false
|
|
$RecoveryCreatedByThisInvocation = $false
|
|
|
|
try {
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $Target)
|
|
|
|
# Create protected empty files BEFORE token redemption. If persistence is
|
|
# not possible, fail before consuming the single-use token.
|
|
$pendingStream = Open-MinerOpsEnrollmentCredentialFileCreateNew `
|
|
-Path $PendingConfigPath -ExpectedParent $Target
|
|
$PendingCreatedByThisInvocation = $true
|
|
$recoveryStream = Open-MinerOpsEnrollmentCredentialFileCreateNew `
|
|
-Path $RecoveryConfigPath -ExpectedParent $Target
|
|
$RecoveryCreatedByThisInvocation = $true
|
|
|
|
$requestBody = @{
|
|
token = $EnrollmentToken
|
|
} | ConvertTo-Json -Compress
|
|
|
|
Write-Host ""
|
|
Write-Host "Redeeming one-time MinerOps enrollment..."
|
|
|
|
$response = Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri "$HubUrl/api/agent/enroll" `
|
|
-ContentType "application/json" `
|
|
-Body $requestBody `
|
|
-TimeoutSec 30
|
|
|
|
$Redeemed = $true
|
|
|
|
if (-not $response.data) {
|
|
throw "MinerOps enrollment returned no Agent data."
|
|
}
|
|
|
|
$agent = $response.data
|
|
|
|
foreach ($required in @(
|
|
"agent_id",
|
|
"agent_secret",
|
|
"site_id",
|
|
"site_name",
|
|
"agent_zone_id",
|
|
"agent_zone_name"
|
|
)) {
|
|
if (-not ([string] $agent.$required).Trim()) {
|
|
throw "MinerOps enrollment response is missing $required."
|
|
}
|
|
}
|
|
|
|
$ranges = @()
|
|
|
|
foreach ($cidr in @($agent.ip_ranges)) {
|
|
$network = ([string] $cidr).Trim()
|
|
|
|
if (-not $network) {
|
|
continue
|
|
}
|
|
|
|
$ranges += [pscustomobject]@{
|
|
name = "$($agent.agent_zone_name) - $network"
|
|
cidr = $network
|
|
enabled = $true
|
|
api_port = 4028
|
|
api3_port = 4433
|
|
}
|
|
}
|
|
|
|
$config = [ordered]@{
|
|
hub_url = $HubUrl
|
|
agent_id = [string] $agent.agent_id
|
|
site_id = [string] $agent.site_id
|
|
agent_zone_id = [string] $agent.agent_zone_id
|
|
site_name = [string] $agent.site_name
|
|
agent_secret = [string] $agent.agent_secret
|
|
poll_interval_seconds = 120
|
|
ip_ranges = @($ranges)
|
|
api3_account = $Api3Account
|
|
api3_password = $Api3PasswordPlain
|
|
api3_port = 4433
|
|
}
|
|
|
|
$configJson = $config | ConvertTo-Json -Depth 20
|
|
# Write a protected recovery copy first. If anything after redemption
|
|
# fails, the permanent Agent credential is not lost with the consumed token.
|
|
Write-MinerOpsEnrollmentCredentialJson `
|
|
-Stream $recoveryStream -Json $configJson
|
|
Write-MinerOpsEnrollmentCredentialJson `
|
|
-Stream $pendingStream -Json $configJson
|
|
$recoveryStream.Dispose()
|
|
$recoveryStream = $null
|
|
$pendingStream.Dispose()
|
|
$pendingStream = $null
|
|
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $Target)
|
|
[IO.File]::Move($PendingConfigPath, $ConfigPath)
|
|
$PendingCreatedByThisInvocation = $false
|
|
Assert-MinerOpsEnrollmentCredentialFile `
|
|
-Path $ConfigPath -ExpectedParent $Target
|
|
|
|
$ConfigPersisted = $true
|
|
|
|
[void](Assert-MinerOpsEnrollmentDirectory -Path $Target)
|
|
Remove-Item -LiteralPath $RecoveryConfigPath -Force
|
|
$RecoveryCreatedByThisInvocation = $false
|
|
|
|
# The extracted bootstrap token is no longer needed once permanent
|
|
# credentials have been persisted locally.
|
|
Remove-Item -LiteralPath $EnrollmentPath -Force
|
|
|
|
$EnrollmentToken = $null
|
|
$requestBody = $null
|
|
$Api3PasswordPlain = $null
|
|
|
|
Write-Host "Permanent Agent identity saved locally."
|
|
Write-Host "Installing MinerOps Agent runtime..."
|
|
|
|
& $Installer
|
|
|
|
$mainTask = Get-ScheduledTask `
|
|
-TaskName "MinerOpsAgent" `
|
|
-ErrorAction Stop
|
|
|
|
if ([string] $mainTask.State -ne "Running") {
|
|
Start-ScheduledTask -TaskName "MinerOpsAgent" -ErrorAction Stop
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "MinerOps Agent enrollment completed."
|
|
Write-Host "Agent ID: $($agent.agent_id)"
|
|
Write-Host "Site: $($agent.site_name)"
|
|
Write-Host "Agent Zone: $($agent.agent_zone_name)"
|
|
Write-Host ""
|
|
Write-Host "Delete the downloaded ZIP and extracted setup folder after verification."
|
|
}
|
|
catch {
|
|
if ($null -ne $pendingStream) {
|
|
$pendingStream.Dispose()
|
|
$pendingStream = $null
|
|
}
|
|
if ($null -ne $recoveryStream) {
|
|
$recoveryStream.Dispose()
|
|
$recoveryStream = $null
|
|
}
|
|
if (-not $Redeemed) {
|
|
foreach ($temporary in @(
|
|
@{ Path = $PendingConfigPath; Created = $PendingCreatedByThisInvocation },
|
|
@{ Path = $RecoveryConfigPath; Created = $RecoveryCreatedByThisInvocation }
|
|
)) {
|
|
if ([bool]$temporary.Created -and
|
|
(Test-Path -LiteralPath ([string]$temporary.Path))) {
|
|
Remove-Item `
|
|
-LiteralPath ([string]$temporary.Path) `
|
|
-Force `
|
|
-ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
elseif ($ConfigPersisted) {
|
|
Write-Warning @"
|
|
The enrollment token was consumed and the permanent Agent identity WAS saved
|
|
to C:\MinerOpsAgent\agent-config.json.
|
|
|
|
Do not create a replacement enrollment. Correct the installation problem and
|
|
rerun install.ps1 from this package.
|
|
"@
|
|
}
|
|
else {
|
|
Write-Warning @"
|
|
The enrollment token was consumed.
|
|
|
|
A protected credential recovery file may exist at:
|
|
C:\MinerOpsAgent\agent-config.recovery.json
|
|
|
|
Do not create or redeem another enrollment until that recovery file has been
|
|
reviewed.
|
|
"@
|
|
}
|
|
|
|
throw
|
|
}
|