diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index 680a3f2e..cf644fd9 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -18,7 +18,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic ### Bugfixes * [#718](https://github.com/Icinga/icinga-powershell-framework/issues/718) Fixes Icinga repository JSON validator to report the correct state of the validation status, in case the JSON is not valid - +* [#725](https://github.com/Icinga/icinga-powershell-framework/pull/725) Fixes Icinga for Windows certificate handling by keeping the .pfx file on the system as created, without using the certificate store. Also fixes handling for providing thumbprints for certificates, which are now loaded directly from the certificate store by also providing a new filter mechanic to fetch the proper certificates from the store ## 1.12.2 (2024-04-10) diff --git a/doc/images/04_knowledgebase/IWKB000018/01_cert_store.png b/doc/images/04_knowledgebase/IWKB000018/01_cert_store.png new file mode 100644 index 00000000..974b6b5a Binary files /dev/null and b/doc/images/04_knowledgebase/IWKB000018/01_cert_store.png differ diff --git a/doc/images/04_knowledgebase/IWKB000018/02_manage_private_keys.png b/doc/images/04_knowledgebase/IWKB000018/02_manage_private_keys.png new file mode 100644 index 00000000..2f2719c7 Binary files /dev/null and b/doc/images/04_knowledgebase/IWKB000018/02_manage_private_keys.png differ diff --git a/doc/images/04_knowledgebase/IWKB000018/03_add_networkservice_user.png b/doc/images/04_knowledgebase/IWKB000018/03_add_networkservice_user.png new file mode 100644 index 00000000..3622e161 Binary files /dev/null and b/doc/images/04_knowledgebase/IWKB000018/03_add_networkservice_user.png differ diff --git a/doc/images/04_knowledgebase/IWKB000018/04_set_permission_and_apply.png b/doc/images/04_knowledgebase/IWKB000018/04_set_permission_and_apply.png new file mode 100644 index 00000000..49feb6c0 Binary files /dev/null and b/doc/images/04_knowledgebase/IWKB000018/04_set_permission_and_apply.png differ diff --git a/doc/knowledgebase/IWKB000018.md b/doc/knowledgebase/IWKB000018.md index 4e22d242..bf0389a2 100644 --- a/doc/knowledgebase/IWKB000018.md +++ b/doc/knowledgebase/IWKB000018.md @@ -26,8 +26,76 @@ Internal ## Reason -This message happens in case the user assigned to run the Icinga for Windows has no sufficient permissions to access the `icingaforwindows.pfx` certificate file or has no permissions to read the private key from the certificate file. +This message happens in case the user assigned to run Icinga for Windows has insufficient permissions to access the given certificates private key. Most commonly, this will happen when Icinga for Windows will run as `NT Authority\NetworkService` while a certificate is loaded from the certificate store. ## Solution -To resolve this issue, you will either have to use [JEA-Profiles](../130-JEA/01-JEA-Profiles.md) or use a different user having enough permissions to access private key to the file. In general, only `LocalSystem` or `Administrators` have access to this key, which is why we highly recommend the use of JEA. +### Use JEA + +Regardless if you are using certificates from the certificate store or the `icingaforwindows.pfx`, we recommend using [JEA-Profiles](../130-JEA/01-JEA-Profiles.md) with a dedicated user like `icinga`. This resolves most permission issues, increases security and is easier to manage. + +### Use icingaforwindows.pfx + +The easiest solution besides using [JEA](../130-JEA/01-JEA-Profiles.md) would to configure the Icinga for Windows background daemon to look for the default certificate to load while starting the REST-Api. This certificate is the `icingaforwindows.pfx` which is by default located at + +``` +C:\Program Files\WindowsPowerShell\Modules\icinga-powershell-framework\certificate\ +``` + +This certificate is generated automatically based on your `Icinga Agent certificate` on the machine during the first installation of Icinga for Windows and updated every day at 1:00 am by a scheduled task in the background. In case the certificate is not present and you want to use it or renew the certificate, you can generate it with the following command + +```powershell +Start-IcingaWindowsScheduledTaskRenewCertificate; +``` + +This will generate the certificate and ensure it is present on the system. If you reconfigured your API to use a specific certificate from the cert store, you can restore the default behavior with + +```powershell +Register-IcingaBackgroundDaemon -Command Start-IcingaWindowsRESTApi; +``` + +Afterwards simply restart Icinga for Windows and the API should be working properly + +```powershell +Restart-IcingaForWindows; +``` + +### Grant permission to user for the private key + +If you want to use a certificate stored inside the certificate store and you receive the above error message, you will have to make sure the corresponding user has access to the `private key` of the certificate. By using [JEA](../130-JEA/01-JEA-Profiles.md), you can skip this part as long as it is not the `NT Authority\NetworkService` user you are having problems with. + +Please make sure as well, that your certificates are stored somewhere in the `LocalMachine` store and **not** in the `CurrentUser` Space. + +To change the permission for the private key of the certificate, proceed as follow + +1. Hint `Windows Key + R` +2. Enter `mmc` into the Run dialog and hit enter (confirm a possible UAC prompt) +3. Inside the mmc, on the top left click on `File` -> `Add/Remove Snap-In` +4. Inside the new dialog look for `Certificates` and click the `Add` button in the middle +5. A new window `Certificates snap-in` will pop up +6. Select `Computer account` and click `Next` +7. Use the `Local computer` and click `Finish` and on the remaining Window `Ok` + +Now you need to navigate to the location on where your certificate is installed to. In our example, we assume it is installed in the `Personal` space + +![Cert Store](../images/04_knowledgebase/IWKB000018/01_cert_store.png) + +Look for the certificate you want to modify the `private key` for and right-click the certificate. + +Now navigate to `All Tasks` -> `Manage Private Keys...` + +![Private Key Managament](../images/04_knowledgebase/IWKB000018/02_manage_private_keys.png) + +In the new window, click the `Add...` button and add the `NT Authority\NetworkService` user to the list of users having access to the private key. Most commonly you have to simply write `NETWORK SERVICE` into the prompt. Change this user for the ony applying to your use case. + +![Private Key Managament](../images/04_knowledgebase/IWKB000018/03_add_networkservice_user.png) + +Make sure to grant the user `Read` and `Full Control` permissions and click `Ok` and `Apply` + +![Private Key Managament](../images/04_knowledgebase/IWKB000018/04_set_permission_and_apply.png) + +Afterwards simply restart Icinga for Windows and the API should be working properly + +```powershell +Restart-IcingaForWindows; +``` diff --git a/jobs/RenewCertificate.ps1 b/jobs/RenewCertificate.ps1 index 00da3b0c..e1c2ccb0 100644 --- a/jobs/RenewCertificate.ps1 +++ b/jobs/RenewCertificate.ps1 @@ -6,11 +6,12 @@ Use-Icinga -Minimal; # To make the configuration of the task as easy as possible, we should fetch # the current configuration of our REST-Api and check if we provide a custom -# certificate file or thumbprint. In case we do, ensure we use this certificate +# certificate file. In case we do, ensure we use this certificate # for the icingaforwindows.pfx creation instead of the auto lookup +# We do only require to check for cert files on the disk, as the cert store +# is fetched automatically [hashtable]$RegisteredBackgroundDaemons = Get-IcingaBackgroundDaemons; [string]$CertificatePath = ''; -[string]$CertificateThumbprint = ''; if ($RegisteredBackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi')) { if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('CertFile')) { @@ -19,15 +20,9 @@ if ($RegisteredBackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi')) { if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('-CertFile')) { $CertificatePath = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['-CertFile']; } - if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('CertThumbprint')) { - $CertificateThumbprint = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['CertThumbprint']; - } - if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('-CertThumbprint')) { - $CertificateThumbprint = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['-CertThumbprint']; - } } -Install-IcingaForWindowsCertificate -CertFile $CertificatePath -CertThumbprint $CertificateThumbprint; +Install-IcingaForWindowsCertificate -CertFile $CertificatePath; # 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 6a00cf91..c2c6eabe 100644 --- a/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 +++ b/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 @@ -128,4 +128,19 @@ function Invoke-IcingaForWindowsMigration() Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.2'); } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.3')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.12.3'; + + # Updates certificate renew task to properly handle changes in the certificate renewal process + Register-IcingaWindowsScheduledTaskRenewCertificate -Force; + Start-Sleep -Seconds 1; + # Enforce the certificate creation to update broken certificates + Start-IcingaWindowsScheduledTaskRenewCertificate; + # Restart the Icinga for Windows service + Start-Sleep -Seconds 2; + Restart-IcingaForWindows; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.3'); + } } diff --git a/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 b/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 index c472b382..04e7f565 100644 --- a/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 +++ b/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 @@ -17,7 +17,7 @@ function Register-IcingaWindowsScheduledTaskRenewCertificate() $ScriptPath = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath '\jobs\RenewCertificate.ps1'; $TaskTrigger = New-ScheduledTaskTrigger -Daily -DaysInterval 1 -At '1am'; $TaskAction = New-ScheduledTaskAction -Execute 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument ([string]::Format("-WindowStyle Hidden -Command &{{ & '{0}' }}", $ScriptPath)); - $TaskPrincipal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-544' -RunLevel 'Highest'; + $TaskPrincipal = New-ScheduledTaskPrincipal -UserId 'S-1-5-18' -RunLevel 'Highest' -LogonType ServiceAccount; $TaskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -StartWhenAvailable; Register-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Force -Principal $TaskPrincipal -Action $TaskAction -Trigger $TaskTrigger -Settings $TaskSettings | Out-Null; diff --git a/lib/daemon/Start-IcingaPowerShellDaemon.psm1 b/lib/daemon/Start-IcingaPowerShellDaemon.psm1 index 5c3f7499..64a849da 100644 --- a/lib/daemon/Start-IcingaPowerShellDaemon.psm1 +++ b/lib/daemon/Start-IcingaPowerShellDaemon.psm1 @@ -42,6 +42,7 @@ function Start-IcingaForWindowsDaemon() 'Certificate' = $null; 'CertFile' = $null; 'CertThumbprint' = $null; + 'CertFilter' = $null; } ); @@ -66,6 +67,7 @@ function Start-IcingaForWindowsDaemon() 'Certificate' = $null; 'CertFile' = $null; 'CertThumbprint' = $null; + 'CertFilter' = $null; } ); diff --git a/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 b/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 index d3fb6102..4f02f3e8 100644 --- a/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 +++ b/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 @@ -2,10 +2,11 @@ function New-IcingaForWindowsRESTApi() { # Allow us to parse the framework global data to this thread param ( - [string]$Address = '', + [string]$Address = '', $Port, - $CertFile, - $CertThumbprint, + $CertFile = $null, + $CertThumbprint = $null, + [PSCustomObject]$CertFilter = $null, $RequireAuth ); @@ -56,28 +57,20 @@ function New-IcingaForWindowsRESTApi() $Global:Icinga.Public.SSL.CertFile = $CertFile; $Global:Icinga.Public.SSL.CertThumbprint = $CertThumbprint; + $Global:Icinga.Public.SSL.CertFilter = $CertFilter; while ($TRUE) { if ($null -eq $Global:Icinga.Public.SSL.Certificate) { - # In case we are not inside a JEA context, use the SSLCertForSocket function to create the certificate file on the fly - # while maintaining the new wait feature. This fix is required, as the NetworkService user has no permssion - # to read the icingaforwindows.pfx file with the private key - if ([string]::IsNullOrEmpty((Get-IcingaJEAContext))) { - $Global:Icinga.Public.SSL.Certificate = Get-IcingaSSLCertForSocket ` - -CertFile $Global:Icinga.Public.SSL.CertFile ` - -CertThumbprint $Global:Icinga.Public.SSL.CertThumbprint; - } else { - $Global:Icinga.Public.SSL.Certificate = Get-IcingaForWindowsCertificate; - } + $Global:Icinga.Public.SSL.Certificate = Get-IcingaForWindowsCertificate; } if ($null -ne $Global:Icinga.Public.SSL.Certificate) { break; } - # Wait 5 minutes and try again - Write-IcingaEventMessage -EventId 2002 -Namespace 'RESTApi' -Objects ($Global:Icinga.Public.SSL.Certificate | Out-String), $Global:Icinga.Public.SSL.CertFile, $Global:Icinga.Public.SSL.CertThumbprint; - Start-Sleep -Seconds (60 * 5); + # Wait 1 minutes and try again + Write-IcingaEventMessage -EventId 2002 -Namespace 'RESTApi' -Objects ($Global:Icinga.Public.SSL.Certificate | Out-String), $Global:Icinga.Public.SSL.CertFile, $Global:Icinga.Public.SSL.CertThumbprint, ($Global:Icinga.Public.SSL.CertFilter | Out-String); + Start-Sleep -Seconds 60; } # Create a background thread to renew the certificate on a regular basis diff --git a/lib/daemons/RestAPI/daemon/Start-IcingaWindowsRESTApi.psm1 b/lib/daemons/RestAPI/daemon/Start-IcingaWindowsRESTApi.psm1 index 394b9c2e..0f5c35b6 100644 --- a/lib/daemons/RestAPI/daemon/Start-IcingaWindowsRESTApi.psm1 +++ b/lib/daemons/RestAPI/daemon/Start-IcingaWindowsRESTApi.psm1 @@ -45,13 +45,14 @@ function Start-IcingaWindowsRESTApi() { param ( - [string]$Address = '', - [int]$Port = 5668, - [string]$CertFile = $null, - [string]$CertThumbprint = $null, - [bool]$RequireAuth = $FALSE, - [int]$ConcurrentThreads = 5, - [int]$Timeout = 120 + [string]$Address = '', + [int]$Port = 5668, + [string]$CertFile = $null, + [string]$CertThumbprint = $null, + [PSCustomObject]$CertFilter = $null, + [bool]$RequireAuth = $FALSE, + [int]$ConcurrentThreads = 5, + [int]$Timeout = 120 ); New-IcingaForWindowsRESTEnvironment -ThreadCount $ConcurrentThreads; @@ -67,6 +68,7 @@ function Start-IcingaWindowsRESTApi() 'Port' = $Port; 'CertFile' = $CertFile; 'CertThumbprint' = $CertThumbprint; + 'CertFilter' = $CertFilter; 'RequireAuth' = $RequireAuth; } ` -Start; diff --git a/lib/daemons/RestAPI/threads/New-IcingaForWindowsCertificateThreadTaskInstance.psm1 b/lib/daemons/RestAPI/threads/New-IcingaForWindowsCertificateThreadTaskInstance.psm1 index 58428adb..929d47ea 100644 --- a/lib/daemons/RestAPI/threads/New-IcingaForWindowsCertificateThreadTaskInstance.psm1 +++ b/lib/daemons/RestAPI/threads/New-IcingaForWindowsCertificateThreadTaskInstance.psm1 @@ -6,16 +6,7 @@ function New-IcingaForWindowsCertificateThreadTaskInstance() # Check every 10 minutes if our certificate is present and update it in case it is # missing or updates have happened - # In case we are not inside a JEA context, use the SSLCertForSocket function to create the certificate file on the fly - # while maintaining the new wait feature. This fix is required, as the NetworkService user has no permssion - # to read the icingaforwindows.pfx file with the private key - if ([string]::IsNullOrEmpty((Get-IcingaJEAContext))) { - $NewIcingaForWindowsCertificate = Get-IcingaSSLCertForSocket ` - -CertFile $Global:Icinga.Public.SSL.CertFile ` - -CertThumbprint $Global:Icinga.Public.SSL.CertThumbprint; - } else { - $NewIcingaForWindowsCertificate = Get-IcingaForWindowsCertificate; - } + $NewIcingaForWindowsCertificate = Get-IcingaForWindowsCertificate; if ($null -ne $NewIcingaForWindowsCertificate) { if ($NewIcingaForWindowsCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $IcingaHostname).ToLower())) { diff --git a/lib/webserver/Get-IcingaForWindowsCertificate.psm1 b/lib/webserver/Get-IcingaForWindowsCertificate.psm1 index 8beeb182..d6e33b98 100644 --- a/lib/webserver/Get-IcingaForWindowsCertificate.psm1 +++ b/lib/webserver/Get-IcingaForWindowsCertificate.psm1 @@ -1,11 +1,65 @@ function Get-IcingaForWindowsCertificate() { - [string]$CertificateFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate'; - [string]$CertificateFile = Join-Path -Path $CertificateFolder -ChildPath 'icingaforwindows.pfx'; + [string]$CertThumbprint = $null; + [PSCustomObject]$CertFilter = $null; + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null; - if (-Not (Test-Path $CertificateFile)) { - return $null; + if ($Global:Icinga.Public.ContainsKey('SSL')) { + $CertThumbprint = $Global:Icinga.Public.SSL.CertThumbprint; + $CertFilter = $Global:Icinga.Public.SSL.CertFilter; } - return ([Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile($CertificateFile)); + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE -Or $null -ne $CertFilter) { + [hashtable]$FilterArgs = @{ + '-Recurse' = $TRUE; + '-Path' = 'cert:\LocalMachine\*'; + } + + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE) { + $FilterArgs.Add('-Include', $CertThumbprint); + } + + [array]$Certificates = Get-ChildItem @FilterArgs; + [DateTime]$CurrentTime = [DateTime]::Now; + + foreach ($cert in $Certificates) { + if ((Test-PSCustomObjectMember -PSObject $CertFilter -Name 'Subject') -And [string]::IsNullOrEmpty($CertFilter.Subject) -eq $FALSE) { + if ($cert.Subject.ToLower() -NotLike $CertFilter.Subject.ToLower()) { + continue; + } + } + + if ((Test-PSCustomObjectMember -PSObject $CertFilter -Name 'Issuer') -And [string]::IsNullOrEmpty($CertFilter.Issuer) -eq $FALSE) { + if ($cert.Issuer.ToLower() -NotLike $CertFilter.Issuer.ToLower()) { + continue; + } + } + + # Certificate expiration date + if ($cert.NotAfter -lt $CurrentTime) { + continue; + } + + # Certificate start date + if ($cert.NotBefore -gt $CurrentTime) { + continue; + } + + $Certificate = $cert; + break; + } + } else { + [string]$CertificateFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate'; + [string]$CertificateFile = Join-Path -Path $CertificateFolder -ChildPath 'icingaforwindows.pfx'; + + if (-Not (Test-Path $CertificateFile)) { + return $null; + } + + $Certificate = ( + New-Object Security.Cryptography.X509Certificates.X509Certificate2 $CertificateFile, '', ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet) + ); + } + + return $Certificate; } diff --git a/lib/webserver/Install-IcingaForWindowsCertificate.psm1 b/lib/webserver/Install-IcingaForWindowsCertificate.psm1 index 64cbd575..c91e42d6 100644 --- a/lib/webserver/Install-IcingaForWindowsCertificate.psm1 +++ b/lib/webserver/Install-IcingaForWindowsCertificate.psm1 @@ -30,24 +30,23 @@ function Install-IcingaForWindowsCertificate() } } + # This is no longer supported as certificates will now be read from the cert store directly + # We just keep the argument for compatibility reasons if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE) { - $Certificate = Get-ChildItem -Path 'cert:\*' -Include $CertThumbprint -Recurse + Write-IcingaDeprecated -Function 'Install-IcingaForWindowsCertificate' -Argument 'CertThumbprint'; + <#$Certificate = Get-ChildItem -Path 'cert:\*' -Include $CertThumbprint -Recurse if ($null -ne $Certificate) { Export-Certificate -Cert $Certificate -FilePath $CertificateFile | Out-Null; - } + }#> + return; } if ([string]::IsNullOrEmpty($CertFile) -And [string]::IsNullOrEmpty($CertThumbprint)) { $IcingaHostCertificate = Get-IcingaAgentHostCertificate; if ([string]::IsNullOrEmpty($IcingaHostCertificate.CertFile) -eq $FALSE) { - $LocalCert = ConvertTo-IcingaX509Certificate -CertFile $IcingaHostCertificate.CertFile -OutFile $CertificateFile -Force; - - Import-PfxCertificate -FilePath $CertificateFile -CertStoreLocation 'Cert:\LocalMachine\My\' -Exportable | Out-Null; - Remove-ItemSecure -Path $CertificateFile -Force | Out-Null; - $Certificate = Get-ChildItem -Path 'cert:\*' -Include $LocalCert.Thumbprint -Recurse - Export-Certificate -Cert $Certificate -FilePath $CertificateFile | Out-Null; + ConvertTo-IcingaX509Certificate -CertFile $IcingaHostCertificate.CertFile -OutFile $CertificateFile -Force | Out-Null; } }