From c95d7baa5efa0c28c2ab5d350b1e7359edc106df Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Tue, 18 Jun 2024 13:35:39 +0200 Subject: [PATCH] Adds support to authenticate with the Icinga for Windows certificate for the REST-Api --- doc/100-General/10-Changelog.md | 1 + jobs/RenewCertificate.ps1 | 23 +++++++- .../Invoke-IcingaForWindowsMigration.psm1 | 9 +++ .../tests/Test-IcingaForWindows.psm1 | 10 ++-- lib/core/logging/Icinga_EventLog_Enums.psm1 | 18 ++++++ lib/webserver/Import-IcingaCAToAuthRoot.psm1 | 18 ++++++ .../Invoke-IcingaForWindowsRESTApi.psm1 | 55 +++++++++++++++++++ .../Test-IcingaCAInstalledToAuthRoot.psm1 | 22 ++++++++ .../Test-IcingaForWindowsCertificate.psm1 | 20 +++++++ 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 lib/webserver/Import-IcingaCAToAuthRoot.psm1 create mode 100644 lib/webserver/Invoke-IcingaForWindowsRESTApi.psm1 create mode 100644 lib/webserver/Test-IcingaCAInstalledToAuthRoot.psm1 create mode 100644 lib/webserver/Test-IcingaForWindowsCertificate.psm1 diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index 8e14916..b40e80a 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -21,6 +21,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic * [#732](https://github.com/Icinga/icinga-powershell-framework/pull/732) Adds support for TLS 1.3 and improves startup response * [#735](https://github.com/Icinga/icinga-powershell-framework/pull/735) Adds support to provide occuring problem event id's for the Eventlog and corresponding acknowledgement id's, providing an indicator if certain issues are resolved or still present +* [#740](https://github.com/Icinga/icinga-powershell-framework/pull/740) Adds new command `Invoke-IcingaForWindowsRESTApi` for easier API communication * [#742](https://github.com/Icinga/icinga-powershell-framework/pull/742) Adds support for the CPU provider to limit the CPU usage to 100% for each thread ## 1.12.3 (2024-04-24) diff --git a/jobs/RenewCertificate.ps1 b/jobs/RenewCertificate.ps1 index e1c2ccb..6aa9ee4 100644 --- a/jobs/RenewCertificate.ps1 +++ b/jobs/RenewCertificate.ps1 @@ -22,7 +22,28 @@ if ($RegisteredBackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi')) { } } -Install-IcingaForWindowsCertificate -CertFile $CertificatePath; +# Wait during the initial run as long as the certificate is not available +while ($TRUE) { + Install-IcingaForWindowsCertificate -CertFile $CertificatePath; + + if ((Test-IcingaForWindowsCertificate) -eq $FALSE) { + Write-IcingaEventMessage -EventId 1508 -Namespace 'Framework'; + Start-Sleep -Seconds 60; + + continue; + } + + break; +} + +# Ensure we import the Icinga ca.crt to the root store, which allows us to use the certificate +# of the agent to connect the the Icinga for Windows API without having to break the certificate trust +[bool]$CAImportSuccess = Import-IcingaCAToAuthRoot; + +if ($CAImportSuccess -eq $FALSE) { + Write-IcingaEventMessage -EventId 1509 -Namespace 'Framework'; + exit 1; +} # Tell the Task-Scheduler that the script was executed fine exit 0; diff --git a/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 b/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 index c2c6eab..35118ee 100644 --- a/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 +++ b/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 @@ -143,4 +143,13 @@ function Invoke-IcingaForWindowsMigration() Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.3'); } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.13.0')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.13.0'; + + # Updates certificate renew task to handle changes made which now stores the Icinga CA inside the cert store + Start-IcingaWindowsScheduledTaskRenewCertificate; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.13.0'); + } } diff --git a/lib/core/icingaagent/tests/Test-IcingaForWindows.psm1 b/lib/core/icingaagent/tests/Test-IcingaForWindows.psm1 index 3eebf4d..e6621ff 100644 --- a/lib/core/icingaagent/tests/Test-IcingaForWindows.psm1 +++ b/lib/core/icingaagent/tests/Test-IcingaForWindows.psm1 @@ -146,6 +146,7 @@ function Test-IcingaForWindows() if ($ResolveProblems) { Write-IcingaConsoleNotice 'Fixing problems with missing not signed Icinga for Windows certificate by re-creating it from the Icinga Agent certificate'; Start-IcingaWindowsScheduledTaskRenewCertificate; + Start-Sleep -Seconds 5; $IfWCertificate = Get-IcingaForWindowsCertificate; if ($IfWCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $Hostname).ToLower())) { @@ -188,17 +189,16 @@ function Test-IcingaForWindows() } Set-IcingaTLSVersion; - Enable-IcingaUntrustedCertificateValidation -SuppressMessages; try { - $ApiResult = Invoke-WebRequest -UseBasicParsing -Uri 'https://localhost:5668/v1' -ErrorAction Stop; - Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows REST-Api responded successfully on "https://localhost:5668/v1"'; + $ApiResult = Invoke-IcingaForWindowsRESTApi; + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows REST-Api responded successfully on this machine'; } catch { if ($IfWService.User.ToLower() -eq 'nt authority\networkservice') { - Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows REST-Api responded with an error on "https://localhost:5668/v1", which is expected when using the default NetworkService account [IWKB000018]: "{0}"', $_.Exception.Message)); + Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows REST-Api responded with an error on this machine, which is expected when using the default NetworkService account [IWKB000018]: "{0}"', $_.Exception.Message)); } else { if ($ResolveProblems -eq $FALSE) { - Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows REST-Api responded with an error on "https://localhost:5668/v1": "{0}"', $_.Exception.Message)); + Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows REST-Api responded with an error on this machine: "{0}"', $_.Exception.Message)); } else { Write-IcingaConsoleNotice 'Fixing problems with Icinga for Windows REST-Api by restarting the Icinga for Windows service'; Restart-IcingaForWindows; diff --git a/lib/core/logging/Icinga_EventLog_Enums.psm1 b/lib/core/logging/Icinga_EventLog_Enums.psm1 index 2e69602..16e2921 100644 --- a/lib/core/logging/Icinga_EventLog_Enums.psm1 +++ b/lib/core/logging/Icinga_EventLog_Enums.psm1 @@ -122,6 +122,18 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'One of the internal Icinga for Windows threads was being active but not responding for at least 5 minutes. The frozen thread has been terminated and restarted.'; 'EventId' = 1507; }; + 1508 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows certificate is pending for signing'; + 'Details' = 'The Icinga for Windows certificate is pending to be signed. This happens if the Icinga Agent certificate is not yet signed by the Icinga CA. The creation will pause for some minutes and try again.'; + 'EventId' = 1508; + }; + 1509 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga CA certificate could not be imported to the AuthRoot store'; + 'Details' = 'The Icinga CA certificate could not be imported to the LocalMachine\AuthRoot store on this device. This will cause all local PowerShell requests to communicate with the API to be untrusted, requring to enable untrusted certificate request as otherwise the communication will fail.'; + 'EventId' = 1509; + }; 1550 = @{ 'EntryType' = 'Error'; 'Message' = 'Unsupported web authentication used'; @@ -164,6 +176,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'An exception occurred while executing Icinga for Windows code inside a JEA context.'; 'EventId' = 1600; }; + 1700 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga Agent hostname is not set inside the constants.conf'; + 'Details' = 'Your current Icinga Agent configuration is missing a crutial configuration part inside your constants.conf, which should set the current configured hostname as NodeName. As long as this configuration is missing, certain Icinga for Windows feature might only partially work. Re-Installing Icinga for Windows might resolve this issue.'; + 'EventId' = 1700; + }; } }; } diff --git a/lib/webserver/Import-IcingaCAToAuthRoot.psm1 b/lib/webserver/Import-IcingaCAToAuthRoot.psm1 new file mode 100644 index 0000000..afd4754 --- /dev/null +++ b/lib/webserver/Import-IcingaCAToAuthRoot.psm1 @@ -0,0 +1,18 @@ +function Import-IcingaCAToAuthRoot() +{ + $IcingaCAFile = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\ca.crt'; + + if ((Test-Path $IcingaCAFile) -eq $FALSE) { + return $FALSE; + } + + if (Test-IcingaCAInstalledToAuthRoot) { + return $TRUE; + } + + Import-Certificate -FilePath $IcingaCAFile -CertStoreLocation 'Cert:\LocalMachine\AuthRoot\' | Out-Null; + + return ( + Test-IcingaCAInstalledToAuthRoot + ); +} diff --git a/lib/webserver/Invoke-IcingaForWindowsRESTApi.psm1 b/lib/webserver/Invoke-IcingaForWindowsRESTApi.psm1 new file mode 100644 index 0000000..2fe2611 --- /dev/null +++ b/lib/webserver/Invoke-IcingaForWindowsRESTApi.psm1 @@ -0,0 +1,55 @@ +function Invoke-IcingaForWindowsRESTApi() +{ + param ( + [string]$Uri = 'v1', + [ValidateSet('GET', 'POST')] + [string]$Method = 'GET', + [hashtable]$Body = @{ } + ); + + if ((Test-IcingaCAInstalledToAuthRoot) -eq $FALSE) { + Write-IcingaConsoleError 'The Icinga CA certificate is not installed to the local machine certificate store. Please run the "Start-IcingaWindowsScheduledTaskRenewCertificate" command to fix this issue.'; + return $null; + } + + Set-IcingaTLSVersion; + Enable-IcingaUntrustedCertificateValidation -SuppressMessages; + + $IcingaForWindowsCertificate = Get-IcingaForWindowsCertificate; + $RestApiPort = 5668; + [int]$Timeout = 20; + $BackgroundDaemons = Get-IcingaBackgroundDaemons; + + if ($null -ne $BackgroundDaemons -And $BackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi')) { + $Daemon = $BackgroundDaemons['Start-IcingaWindowsRESTApi']; + + # Fetch our deamon configuration + if ($Daemon.ContainsKey('-Port')) { + $RestApiPort = $Daemon['-Port']; + } elseif ($Daemon.ContainsKey('Port')) { + $RestApiPort = $Daemon['Port']; + } + } + + [string]$WebBaseURL = [string]::Format('https://localhost:{0}', $RestApiPort); + + $WebBaseURL = Join-WebPath -Path $WebBaseURL -ChildPath $Uri; + + [hashtable]$Arguments = @{ + '-Method' = $Method; + '-UseBasicParsing' = $TRUE; + '-Uri' = $WebBaseURL; + '-ContentType' = 'application/json'; + '-TimeoutSec' = $Timeout; + '-Certificate' = $IcingaForWindowsCertificate; + '-ErrorAction' = 'Stop'; + }; + + if ($null -ne $Body -And $Body.Count -ne 0 -And $Method -eq 'POST') { + $Arguments.Add('-Body', (ConvertTo-JsonUTF8Bytes -InputObject $Body -Depth 100 -Compress)); + } + + return ( + Invoke-WebRequest @Arguments + ); +} diff --git a/lib/webserver/Test-IcingaCAInstalledToAuthRoot.psm1 b/lib/webserver/Test-IcingaCAInstalledToAuthRoot.psm1 new file mode 100644 index 0000000..1e1bc2c --- /dev/null +++ b/lib/webserver/Test-IcingaCAInstalledToAuthRoot.psm1 @@ -0,0 +1,22 @@ +function Test-IcingaCAInstalledToAuthRoot() +{ + $IcingaCAFile = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\ca.crt'; + + if ((Test-Path $IcingaCAFile) -eq $FALSE) { + return $FALSE; + } + + $IcingaCACert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 $IcingaCAFile; + + [bool]$IcingaCAInstalled = $FALSE; + + Get-ChildItem -Recurse -Path 'Cert:\LocalMachine\AuthRoot\' | Where-Object { + if ($_.Thumbprint -eq $IcingaCACert.Thumbprint) { + $IcingaCAInstalled = $TRUE; + } + }; + + $IcingaCACert = $null; + + return $IcingaCAInstalled; +} diff --git a/lib/webserver/Test-IcingaForWindowsCertificate.psm1 b/lib/webserver/Test-IcingaForWindowsCertificate.psm1 new file mode 100644 index 0000000..9a2d8ea --- /dev/null +++ b/lib/webserver/Test-IcingaForWindowsCertificate.psm1 @@ -0,0 +1,20 @@ +function Test-IcingaForWindowsCertificate() +{ + $IfWCertificate = Get-IcingaForWindowsCertificate; + $Hostname = Get-IcingaHostname -ReadConstants; + + if ([string]::IsNullOrEmpty($Hostname)) { + Write-IcingaEventMessage -EventId 1700 -Namespace 'Framework'; + return $FALSE; + } + + if ($null -eq $IfWCertificate) { + return $FALSE; + } + + if ($IfWCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $Hostname).ToLower())) { + return $FALSE; + } + + return $TRUE; +}