diff --git a/src/modules/SdnDiag.Common/private/Confirm-IsCertSelfSigned.ps1 b/src/modules/SdnDiag.Common/private/Confirm-IsCertSelfSigned.ps1 new file mode 100644 index 00000000..3327f9aa --- /dev/null +++ b/src/modules/SdnDiag.Common/private/Confirm-IsCertSelfSigned.ps1 @@ -0,0 +1,14 @@ +function Confirm-IsCertSelfSigned { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate + ) + + if ($Certificate.Issuer -eq $Certificate.Subject) { + "Detected the certificate subject and issuer are the same. Setting SelfSigned to true" | Trace-Output -Level:Verbose + return $true + } + + return $false +} diff --git a/src/modules/SdnDiag.Common/private/Copy-CertificateToFabric.ps1 b/src/modules/SdnDiag.Common/private/Copy-CertificateToFabric.ps1 index 312def8d..2e12ee42 100644 --- a/src/modules/SdnDiag.Common/private/Copy-CertificateToFabric.ps1 +++ b/src/modules/SdnDiag.Common/private/Copy-CertificateToFabric.ps1 @@ -43,6 +43,21 @@ function Copy-CertificateToFabric { $Credential = [System.Management.Automation.PSCredential]::Empty ) + $createRemoteDirectorySB = { + param([Parameter(Position = 0)][String]$param1) + if (-NOT (Test-Path -Path $param1 -PathType Container)) { + New-Item -Path $param1 -ItemType Directory -Force + } + } + + # scriptblock to import the certificate + # this function will automatically install the certificate to the localmachine\root cert directory + # if the certificate passed to it is self-signed and it is being installed to localmachine\my cert directory + $importCertSB = { + param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][SecureString]$param2, [Parameter(Position = 2)][String]$param3) + Import-SdnCertificate -FilePath $param1 -CertPassword $param2 -CertStore $param3 + } + # if we are installing the rest certificate and need to seed certificate to southbound devices # then define the variables to know which nodes must be updated if ($PSCmdlet.ParameterSetName -ieq 'NetworkControllerRest' -and $InstallToSouthboundDevices) { @@ -57,6 +72,8 @@ function Copy-CertificateToFabric { } $certFileInfo = Get-Item -Path $CertFile -ErrorAction Stop + [System.String]$remoteFilePath = Join-Path -Path $certFileInfo.Directory.FullName -ChildPath $certFileInfo.Name + switch ($certFileInfo.Extension) { '.pfx' { if ($CertPassword) { @@ -79,151 +96,68 @@ function Copy-CertificateToFabric { switch ($PSCmdlet.ParameterSetName) { 'LoadBalancerMuxNode' { - foreach ($controller in $FabricDetails.NetworkController) { - # if the certificate being passed is self-signed, we will need to copy the certificate to the other controller nodes - # within the fabric and install under localmachine\root as appropriate - if ($certData.Subject -ieq $certData.Issuer) { - "Importing certificate [Subject: {0} Thumbprint:{1}] to {2}" -f ` - $certData.Subject, $certData.Thumbprint, $controller | Trace-Output - - [System.String]$remoteFilePath = Join-Path -Path $certFileInfo.Directory.FullName -ChildPath $certFileInfo.Name - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - if (-NOT (Test-Path -Path $param1 -PathType Container)) { - New-Item -Path $param1 -ItemType Directory -Force - } - } -ArgumentList $certFileInfo.Directory.FullName - - Copy-FileToRemoteComputer -ComputerName $controller -Credential $Credential -Path $certFileInfo.FullName -Destination $remoteFilePath - - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][SecureString]$param2, [Parameter(Position = 2)][String]$param3) - Import-SdnCertificate -FilePath $param1 -CertPassword $param2 -CertStore $param3 - } -ArgumentList @($remoteFilePath, $CertPassword, 'Cert:\LocalMachine\Root') -ErrorAction Stop - } - - else { - "No action required for {0}" -f $certData.Thumbprint | Trace-Output -Level:Verbose - } + if (Confirm-IsCertSelfSigned -Certificate $certData) { + $certStore = 'Cert:\LocalMachine\Root' + $computersToInstallCert = $FabricDetails.NetworkController } } 'NetworkControllerRest' { - # copy the pfx certificate for the rest certificate to all network controllers within the cluster - # and import to localmachine\my cert directory - foreach ($controller in $FabricDetails.NetworkController) { - "Processing {0}" -f $controller | Trace-Output -Level:Verbose - - "[REST CERT] Importing certificate [Subject: {0} Thumbprint:{1}] to {2}" -f ` - $certData.Subject, $certData.Thumbprint, $controller | Trace-Output - - if (Test-ComputerNameIsLocal -ComputerName $controller) { - $importCert = Import-SdnCertificate -FilePath $certFileInfo.FullName -CertPassword $CertPassword -CertStore 'Cert:\LocalMachine\My' - - # if the certificate was detected as self signed - # we will then copy the .cer file returned from the previous command to all the southbound nodes to install - if ($importCert.SelfSigned -and $InstallToSouthboundDevices) { - Install-SdnDiagnostics -ComputerName $southBoundNodes -Credential $Credential -ErrorAction Stop - - "[REST CERT] Installing self-signed certificate to southbound devices" | Trace-Output - Invoke-PSRemoteCommand -ComputerName $southBoundNodes -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - if (-NOT (Test-Path -Path $param1 -PathType Container)) { - $null = New-Item -Path $param1 -ItemType Directory -Force - } - } -ArgumentList $importCert.CerFileInfo.Directory.FullName - - foreach ($sbNode in $southBoundNodes) { - "[REST CERT] Installing self-signed certificate to {0}" -f $sbNode | Trace-Output - Copy-FileToRemoteComputer -ComputerName $sbNode -Credential $Credential -Path $importCert.CerFileInfo.FullName -Destination $importCert.CerFileInfo.FullName - $null = Invoke-PSRemoteCommand -ComputerName $sbNode -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1,[Parameter(Position = 1)][String]$param2) - Import-SdnCertificate -FilePath $param1 -CertStore $param2 - } -ArgumentList @($importCert.CerFileInfo.FullName, 'Cert:\LocalMachine\Root') -ErrorAction Stop - } - } + if (Confirm-IsCertSelfSigned -Certificate $certData) { + if ($InstallToSouthboundDevices) { + # for southbound devices, if the certificate is self-signed, we will install the certificate under the localmachine\root cert directory + $certStore = 'Cert:\LocalMachine\Root' + $computersToInstallCert = $southBoundNodes } else { - [System.String]$remoteFilePath = Join-Path -Path $certFileInfo.Directory.FullName -ChildPath $certFileInfo.Name - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - if (-NOT (Test-Path -Path $param1 -PathType Container)) { - New-Item -Path $param1 -ItemType Directory -Force - } - } -ArgumentList $certFileInfo.Directory.FullName - - Copy-FileToRemoteComputer -ComputerName $controller -Credential $Credential -Path $certFileInfo.FullName -Destination $remoteFilePath - - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][SecureString]$param2, [Parameter(Position = 2)][String]$param3) - Import-SdnCertificate -FilePath $param1 -CertPassword $param2 -CertStore $param3 - } -ArgumentList @($remoteFilePath, $CertPassword, 'Cert:\LocalMachine\My') + # for network controller, we will install the certificate under the localmachine\my cert directory + $certStore = 'Cert:\LocalMachine\My' + $computersToInstallCert = $FabricDetails.NetworkController } } } 'NetworkControllerNode' { - foreach ($controller in $FabricDetails.NetworkController) { - "Processing {0}" -f $controller | Trace-Output -Level:Verbose - - # if the certificate being passed is self-signed, we will need to copy the certificate to the other controller nodes - # within the fabric and install under localmachine\root as appropriate - if ($certData.Subject -ieq $certData.Issuer) { - "Importing certificate [Subject: {0} Thumbprint:{1}] to {2}" -f ` - $certData.Subject, $certData.Thumbprint, $controller | Trace-Output - - [System.String]$remoteFilePath = Join-Path -Path $certFileInfo.Directory.FullName -ChildPath $certFileInfo.Name - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - if (-NOT (Test-Path -Path $param1 -PathType Container)) { - New-Item -Path $param1 -ItemType Directory -Force - } - } -ArgumentList $certFileInfo.Directory.FullName - - Copy-FileToRemoteComputer -ComputerName $controller -Credential $Credential -Path $certFileInfo.FullName -Destination $remoteFilePath - - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][SecureString]$param2, [Parameter(Position = 2)][String]$param3) - Import-SdnCertificate -FilePath $param1 -CertPassword $param2 -CertStore $param3 - } -ArgumentList @($remoteFilePath, $CertPassword, 'Cert:\LocalMachine\Root') -ErrorAction Stop - } - - else { - "No action required for {0}" -f $certData.Thumbprint | Trace-Output -Level:Verbose - } + if (Confirm-IsCertSelfSigned -Certificate $certData) { + $certStore = 'Cert:\LocalMachine\Root' + $computersToInstallCert = $FabricDetails.NetworkController } } # for ServerNodes, we must distribute the server certificate and install to the cert:\localmachine\root directory on each of the # network controller nodes 'ServerNode' { - foreach ($controller in $FabricDetails.NetworkController) { - # if the certificate being passed is self-signed, we will need to copy the certificate to the other controller nodes - # within the fabric and install under localmachine\root as appropriate - if ($certData.Subject -ieq $certData.Issuer) { - "Importing certificate [Subject: {0} Thumbprint:{1}] to {2}" -f ` - $certData.Subject, $certData.Thumbprint, $controller | Trace-Output - - [System.String]$remoteFilePath = Join-Path -Path $certFileInfo.Directory.FullName -ChildPath $certFileInfo.Name - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - if (-NOT (Test-Path -Path $param1 -PathType Container)) { - New-Item -Path $param1 -ItemType Directory -Force - } - } -ArgumentList $certFileInfo.Directory.FullName - - Copy-FileToRemoteComputer -ComputerName $controller -Credential $Credential -Path $certFileInfo.FullName -Destination $remoteFilePath - - $null = Invoke-PSRemoteCommand -ComputerName $controller -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][SecureString]$param2, [Parameter(Position = 2)][String]$param3) - Import-SdnCertificate -FilePath $param1 -CertPassword $param2 -CertStore $param3 - } -ArgumentList @($remoteFilePath, $CertPassword, 'Cert:\LocalMachine\Root') -ErrorAction Stop - } - - else { - "No action required for {0}" -f $certData.Thumbprint | Trace-Output -Level:Verbose - } + if (Confirm-IsCertSelfSigned -Certificate $certData) { + $certStore = 'Cert:\LocalMachine\Root' + $computersToInstallCert = $FabricDetails.NetworkController } } } + + # create the remote directory we need to copy certificate to + Invoke-PSRemoteCommand @{ + ComputerName = $computersToInstallCert + Credential = $Credential + ScriptBlock = $createRemoteDirectorySB + ArgumentList = @($certFileInfo.Directory.FullName) + ErrorAction = 'Stop' + } + + # copy the file + Copy-FileToRemoteComputer @{ + ComputerName = $computersToInstallCert + Credential = $Credential + Path = $certFileInfo.FullName + Destination = $remoteFilePath + ErrorAction = 'Stop' + } + + # import the certificate + Invoke-PSRemoteCommand @{ + ComputerName = $computersToInstallCert + Credential = $Credential + ScriptBlock = $importCertSB + ArgumentList = @($remoteFilePath, $CertPassword, $certStore) + ErrorAction = 'Stop' + } } diff --git a/src/modules/SdnDiag.Common/public/Get-SdnCertificate.ps1 b/src/modules/SdnDiag.Common/public/Get-SdnCertificate.ps1 index e95e9c2c..35f6c5f0 100644 --- a/src/modules/SdnDiag.Common/public/Get-SdnCertificate.ps1 +++ b/src/modules/SdnDiag.Common/public/Get-SdnCertificate.ps1 @@ -29,26 +29,53 @@ function Get-SdnCertificate { [Parameter(Mandatory = $false, ParameterSetName = 'Thumbprint')] [ValidateNotNullorEmpty()] - [System.String]$Thumbprint + [System.String]$Thumbprint, + + [Parameter(Mandatory = $false, ParameterSetName = 'Default')] + [Parameter(Mandatory = $false, ParameterSetName = 'Subject')] + [Parameter(Mandatory = $false, ParameterSetName = 'Thumbprint')] + [switch]$NetworkControllerOid ) + [string]$objectIdentifier = @('1.3.6.1.4.1.311.95.1.1.1') # this is a custom OID used for Network Controller + $array = @() + try { - $certificateList = Get-ChildItem -Path $Path -Recurse | Where-Object {$_.PSISContainer -eq $false} -ErrorAction Stop + $certificateList = Get-ChildItem -Path $Path | Where-Object {$_.PSISContainer -eq $false} -ErrorAction Ignore + if ($null -eq $certificateList) { + return $null + } + + if ($NetworkControllerOid) { + $certificateList | ForEach-Object { + if ($objectIdentifier -iin $_.EnhancedKeyUsageList.ObjectId) { + $array += $_ + } + } + + # if no certificates are found based on the OID, search based on other criteria + if (!$array) { + "Unable to locate certificates that match Network Controller OID: {0}. Searching based on other criteria." -f $objectIdentifier | Trace-Output -Level:Warning + $array = $certificateList + } + } + else { + $array = $certificateList + } switch ($PSCmdlet.ParameterSetName) { 'Subject' { - $filteredCert = $certificateList | Where-Object {$_.Subject -ieq $Subject} + $filteredCert = $array | Where-Object {$_.Subject -ieq $Subject} } 'Thumbprint' { - $filteredCert = $certificateList | Where-Object {$_.Thumbprint -ieq $Thumbprint} + $filteredCert = $array | Where-Object {$_.Thumbprint -ieq $Thumbprint} } default { - return $certificateList + return $array } } if ($null -eq $filteredCert) { - "Unable to locate certificate using {0}" -f $PSCmdlet.ParameterSetName | Trace-Output -Level:Warning return $null } diff --git a/src/modules/SdnDiag.Common/public/Import-SdnCertificate.ps1 b/src/modules/SdnDiag.Common/public/Import-SdnCertificate.ps1 index 9c98e94c..c28d7b38 100644 --- a/src/modules/SdnDiag.Common/public/Import-SdnCertificate.ps1 +++ b/src/modules/SdnDiag.Common/public/Import-SdnCertificate.ps1 @@ -14,7 +14,7 @@ function Import-SdnCertificate { PS> Import-SdnCertificate -FilePath c:\certs\cert.pfx -CertStore Cert:\LocalMachine\Root -Password $secureString #> - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory = $true)] [System.String]$FilePath, @@ -27,76 +27,81 @@ function Import-SdnCertificate { ) $trustedRootStore = 'Cert:\LocalMachine\Root' - $fileInfo = Get-Item -Path $FilePath - - $certObject = @{ + $certObject = [PSCustomObject]@{ SelfSigned = $false CertInfo = $null - CerFileInfo = $null + SelfSignedCertFileInfo = $null } - switch ($fileInfo.Extension) { - '.pfx' { - if ($CertPassword) { - $certData = (Get-PfxData -FilePath $fileInfo.FullName -Password $CertPassword).EndEntityCertificates - } - else { - $certData = Get-PfxCertificate -FilePath $fileInfo.FullName + try { + $fileInfo = Get-Item -Path $FilePath -ErrorAction Stop + switch ($fileInfo.Extension) { + '.pfx' { + if ($CertPassword) { + $certData = (Get-PfxData -FilePath $fileInfo.FullName -Password $CertPassword).EndEntityCertificates + } + else { + $certData = Get-PfxCertificate -FilePath $fileInfo.FullName + } } - } - '.cer' { - $certData = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $certData.Import($fileInfo) - } + '.cer' { + $certData = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $certData.Import($fileInfo) + } - default { - throw New-Object System.NotSupportedException("Unsupported certificate extension") + default { + throw New-Object System.NotSupportedException("Unsupported certificate extension") + } } - } - $certExists = Get-ChildItem -Path $CertStore | Where-Object {$_.Thumbprint -ieq $certData.Thumbprint} - if ($certExists) { - "{0} already exists under {1}" -f $certExists.Thumbprint, $CertStore | Trace-Output -Level:Verbose - $certObject.CertInfo = $certExists - } - else { - "Importing {0} to {1}" -f $certData.Thumbprint, $CertStore | Trace-Output - if ($certData.HasPrivateKey) { - $importCert = Import-PfxCertificate -FilePath $fileInfo.FullName -CertStoreLocation $CertStore -Password $CertPassword -Exportable -ErrorAction Stop - Set-SdnCertificateAcl -Path $CertStore -Thumbprint $importCert.Thumbprint + $certExists = Get-SdnCertificate -Path $CertStore -Thumbprint $certData.Thumbprint + if ($certExists) { + $certObject.CertInfo = $certExists } else { - $importCert = Import-Certificate -FilePath $fileInfo.FullName -CertStoreLocation $CertStore -ErrorAction Stop - } + "Importing [Subject: $($_.Subject), Thumbprint: $($_.Thumbprint)] to $CertStore" | Trace-Output + if ($certData.HasPrivateKey) { + $importCert = Import-PfxCertificate -FilePath $fileInfo.FullName -CertStoreLocation $CertStore -Password $CertPassword -Exportable -ErrorAction Stop + Set-SdnCertificateAcl -Path $CertStore -Thumbprint $importCert.Thumbprint + } + else { + $importCert = Import-Certificate -FilePath $fileInfo.FullName -CertStoreLocation $CertStore -ErrorAction Stop + } - $certObject.CertInfo = $importCert - } + $certObject.CertInfo = $importCert + } - # determine if the certificates being used are self signed - if ($certObject.CertInfo.Subject -ieq $certObject.CertInfo.Issuer) { - "Detected the certificate subject and issuer are the same. Setting SelfSigned to true" | Trace-Output -Level:Verbose - $certObject.SelfSigned = $true + # determine if the certificates being used are self signed + if (Confirm-IsCertSelfSigned -Certificate $certObject.CertInfo) { + $certObject.SelfSigned = $true - # check to see if we installed to root store with above operation - # if it is not, then we want to check the root store to see if this certificate has already been installed - # and finally if does not exist, then export the certificate from current store and import into trusted root store - if ($CertStore -ine $trustedRootStore) { - $selfSignedCerExists = Get-ChildItem -Path $trustedRootStore | Where-Object {$_.Thumbprint -ieq $certObject.CertInfo.Thumbprint} - [System.String]$selfSignedCerPath = "{0}\{1}.cer" -f (Split-Path $fileInfo.FullName -Parent), ($certObject.CertInfo.Subject).Replace('=','_') - $selfSignedCer = Export-Certificate -Cert $certObject.CertInfo -FilePath $selfSignedCerPath -ErrorAction Stop - $certObject.CerFileInfo = $selfSignedCer + # check to see if we installed to root store with above operation + # if it is not, then we want to check the root store to see if this certificate has already been installed + # and finally if does not exist, then export the certificate from current store and import into trusted root store + if ($CertStore -ine $trustedRootStore) { + $selfSignedCerExists = Get-SdnCertificate -Path $trustedRootStore -Thumbprint $certObject.CertInfo.Thumbprint + [System.String]$selfSignedCerPath = "{0}\{1}.cer" -f (Split-Path $fileInfo.FullName -Parent), ($certObject.CertInfo.Subject).Replace('=','_') + $selfSignedCer = Export-Certificate -Cert $certObject.CertInfo -FilePath $selfSignedCerPath -ErrorAction Stop + $certObject.SelfSignedCertFileInfo = $selfSignedCer - if (-NOT ($selfSignedCerExists)) { - # import the certificate to the trusted root store - "Importing public key to {0}" -f $trustedRootStore | Trace-Output - $null = Import-Certificate -FilePath $selfSignedCer.FullName -CertStoreLocation $trustedRootStore -ErrorAction Stop - } - else { - "{0} already exists under {1}" -f $certObject.CertInfo.Thumbprint, $trustedRootStore | Trace-Output -Level:Verbose + if (-NOT ($selfSignedCerExists)) { + # import the certificate to the trusted root store + "Importing public key to {0}" -f $trustedRootStore | Trace-Output + $null = Import-Certificate -FilePath $selfSignedCer.FullName -CertStoreLocation $trustedRootStore -ErrorAction Stop + } + else { + "{0} already exists under {1}" -f $certObject.CertInfo.Thumbprint, $trustedRootStore | Trace-Output -Level:Verbose + } } } + + return $certObject + } + catch { + $_ | Trace-Exception + $_ | Write-Error } - return $certObject + return $null } diff --git a/src/modules/SdnDiag.Common/public/New-SdnCertificate.ps1 b/src/modules/SdnDiag.Common/public/New-SdnSelfSignedCertificate.ps1 similarity index 84% rename from src/modules/SdnDiag.Common/public/New-SdnCertificate.ps1 rename to src/modules/SdnDiag.Common/public/New-SdnSelfSignedCertificate.ps1 index d2899879..29686b46 100644 --- a/src/modules/SdnDiag.Common/public/New-SdnCertificate.ps1 +++ b/src/modules/SdnDiag.Common/public/New-SdnSelfSignedCertificate.ps1 @@ -1,4 +1,4 @@ -function New-SdnCertificate { +function New-SdnSelfSignedCertificate { <# .SYNOPSIS Creates a new self-signed certificate for use with SDN fabric. @@ -9,7 +9,7 @@ function New-SdnCertificate { .PARAMETER NotAfter Specifies the date and time, as a DateTime object, that the certificate expires. To obtain a DateTime object, use the Get-Date cmdlet. The default value for this parameter is one year after the certificate was created. .EXAMPLE - PS> New-SdnCertificate -Subject rest.sdn.contoso -CertStoreLocation Cert:\LocalMachine\My + PS> New-SdnSelfSignedCertificate -Subject rest.sdn.contoso -CertStoreLocation Cert:\LocalMachine\My #> [CmdletBinding()] @@ -34,6 +34,10 @@ function New-SdnCertificate { try { "Generating certificate with subject {0} under {1}" -f $Subject, $CertStoreLocation | Trace-Output + # create new self signed certificate with the following EnhancedKeyUsageList + # 1.3.6.1.5.5.7.3.1 - Server Authentication OID + # 1.3.6.1.5.5.7.3.2 - Client Authentication OID + # 1.3.6.1.4.1.311.95.1.1.1 - Network Controller OID $selfSignedCert = New-SelfSignedCertificate -Type Custom -KeySpec KeyExchange -Subject $Subject ` -KeyExportPolicy Exportable -HashAlgorithm sha256 -KeyLength 2048 ` -CertStoreLocation $CertStoreLocation -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1,1.3.6.1.5.5.7.3.2,1.3.6.1.4.1.311.95.1.1.1") ` diff --git a/src/modules/SdnDiag.Common/public/Remove-SdnCertificate.ps1 b/src/modules/SdnDiag.Common/public/Remove-SdnCertificate.ps1 new file mode 100644 index 00000000..3430ce92 --- /dev/null +++ b/src/modules/SdnDiag.Common/public/Remove-SdnCertificate.ps1 @@ -0,0 +1,71 @@ +function Remove-SdnCertificate { + <# + .SYNOPSIS + Removes a certificate from the certificate store that contains a custom Network Controller OID. + .PARAMETER Path + Defines the path within the certificate store. Path is expected to start with cert:\. + .PARAMETER Thumbprint + Specifies the thumbprint of the certificate to remove. + .PARAMETER Subject + Specifies the subject of the certificate to remove. + .EXAMPLE + PS> Remove-SdnCertificate -Path "Cert:\LocalMachine\My" -Thumbprint "1234567890ABCDEF1234567890ABCDEF12345678" + .EXAMPLE + PS> Remove-SdnCertificate -Path "Cert:\LocalMachine\My" -Subject "rest.sdn.contoso" + #> + + [CmdletBinding(DefaultParameterSetName = 'Thumbprint', SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Thumbprint')] + [Parameter(Mandatory = $true, ParameterSetName = 'Subject')] + [ValidateScript({ + if ($_ -notlike "cert:\*") { + throw New-Object System.FormatException("Invalid path") + } + + return $true + })] + [System.String]$Path, + + [Parameter(Mandatory = $true, ParameterSetName = 'Thumbprint')] + [ValidateNotNullorEmpty()] + [System.String]$Thumbprint, + + [Parameter(Mandatory = $true, ParameterSetName = 'Subject')] + [ValidateNotNullorEmpty()] + [System.String]$Subject + ) + + $params = @{ + Path = $Path + NetworkControllerOid = $true + } + switch ($PSCmdlet.ParameterSetName) { + 'Thumbprint' { + $params.Add('Thumbprint', $Thumbprint) + } + 'Subject' { + $params.Add('Subject', $Subject) + } + } + + try { + $cert = Get-SdnCertificate @params + if ($cert) { + $cert | ForEach-Object { + $message = "Certificate [Subject: $($_.Subject), Thumbprint: $($_.Thumbprint)]" + if ($PSCmdlet.ShouldProcess($message)) { + "Removing certificate [Subject: $($_.Subject), Thumbprint: $($_.Thumbprint)] from $Path" | Trace-Output -Level:Verbose + $_ | Remove-Item + } + } + } + else { + "No certificate found with with the specified $($PSCmdlet.ParameterSetName)" | Trace-Output -Level:Warning + } + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} diff --git a/src/modules/SdnDiag.LoadBalancerMux/public/Get-SdnMuxCertificate.ps1 b/src/modules/SdnDiag.LoadBalancerMux/public/Get-SdnMuxCertificate.ps1 index 9fee1a88..5ee815b5 100644 --- a/src/modules/SdnDiag.LoadBalancerMux/public/Get-SdnMuxCertificate.ps1 +++ b/src/modules/SdnDiag.LoadBalancerMux/public/Get-SdnMuxCertificate.ps1 @@ -1,16 +1,31 @@ function Get-SdnMuxCertificate { <# .SYNOPSIS - Returns the certificate used by the SDN Load Balancer Mux. + Returns the certificate used by the SDN Load Balancer Mux. + .PARAMETER NetworkControllerOid + Specifies to return only the certificate that has the specified Network Controller OID. #> [CmdletBinding()] - param () + param ( + [Parameter(Mandatory = $false)] + [switch]$NetworkControllerOid + ) try { $muxCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\SlbMux' -Name 'MuxCert' $subjectName = "CN={0}" -f $muxCert - $certificate = Get-SdnCertificate -Subject $subjectName -Path 'Cert:\LocalMachine\My' + $certificate = Get-SdnCertificate -Subject $subjectName -Path 'Cert:\LocalMachine\My' -NetworkControllerOid:$NetworkControllerOid + + if ($null -eq $certificate) { + if ($NetworkControllerOid) { + throw New-Object System.NullReferenceException("Failed to locate certificate for Load Balancer Mux containing Network Controller OID") + } + else { + throw New-Object System.NullReferenceException("Failed to locate certificate for Load Balancer Mux") + } + } + return $certificate } catch { diff --git a/src/modules/SdnDiag.LoadBalancerMux/public/New-SdnMuxCertificate.ps1 b/src/modules/SdnDiag.LoadBalancerMux/public/New-SdnMuxCertificate.ps1 index 1dd66009..6a0db031 100644 --- a/src/modules/SdnDiag.LoadBalancerMux/public/New-SdnMuxCertificate.ps1 +++ b/src/modules/SdnDiag.LoadBalancerMux/public/New-SdnMuxCertificate.ps1 @@ -17,7 +17,7 @@ function New-SdnMuxCertificate { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] - [datetime]$NotAfter = (Get-Date).AddYears(3), + [datetime]$NotAfter = (Get-Date).AddYears(1), [Parameter(Mandatory = $false)] [System.String]$Path = "$(Get-WorkingDirectory)\MuxCert_{0}" -f (Get-FormattedDateTimeUTC), @@ -45,19 +45,19 @@ function New-SdnMuxCertificate { try { if (-NOT (Test-Path -Path $Path -PathType Container)) { "Creating directory {0}" -f $Path | Trace-Output - $CertPath = New-Item -Path $Path -ItemType Directory -Force + $certPath = New-Item -Path $Path -ItemType Directory -Force } else { - $CertPath = Get-Item -Path $Path + $certPath = Get-Item -Path $Path } - $muxCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\SlbMux' -Name 'MuxCert' + $muxCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\SlbMux' -Name 'MuxCert' -ErrorAction Stop $subjectName = "CN={0}" -f $muxCert - $certificate = New-SdnCertificate -Subject $subjectName -NotAfter $NotAfter + $certificate = New-SdnSelfSignedCertificate -Subject $subjectName -NotAfter $NotAfter # after the certificate has been generated, we want to export the certificate and save the file to directory # This allows the rest of the function to pick up these files and perform the steps as normal - [System.String]$cerFilePath = "$(Join-Path -Path $CertPath.FullName -ChildPath $subjectName.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).cer" + [System.String]$cerFilePath = "$(Join-Path -Path $certPath.FullName -ChildPath $subjectName.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).cer" "Exporting certificate to {0}" -f $cerFilePath | Trace-Output $exportedCertificate = Export-Certificate -Cert $certificate -FilePath $cerFilePath -Type CERT Copy-CertificateToFabric -CertFile $exportedCertificate.FullName -FabricDetails $FabricDetails -LoadBalancerMuxNodeCert -Credential $Credential @@ -73,4 +73,6 @@ function New-SdnMuxCertificate { $_ | Trace-Exception $_ | Write-Error } + + return $null } diff --git a/src/modules/SdnDiag.LoadBalancerMux/public/Start-SdnMuxCertificateRotation.ps1 b/src/modules/SdnDiag.LoadBalancerMux/public/Start-SdnMuxCertificateRotation.ps1 index c0255127..5f4f285d 100644 --- a/src/modules/SdnDiag.LoadBalancerMux/public/Start-SdnMuxCertificateRotation.ps1 +++ b/src/modules/SdnDiag.LoadBalancerMux/public/Start-SdnMuxCertificateRotation.ps1 @@ -6,86 +6,46 @@ function Start-SdnMuxCertificateRotation { Specifies a user account that has permission to perform this action on the Load Balancer Mux and Network Controller nodes. The default is the current user. .PARAMETER NcRestCredential Specifies a user account that has permission to access the northbound NC API interface. The default is the current user. - .PARAMETER CertPath - Path directory where certificate(s) .pfx files are located for use with certificate rotation. .PARAMETER GenerateCertificate Switch to determine if certificate rotate function should generate self-signed certificates. .PARAMETER CertPassword SecureString password for accessing the .pfx files, or if using -GenerateCertificate, what the .pfx files will be encrypted with. .PARAMETER NotAfter Expiration date when using -GenerateCertificate. If ommited, defaults to 3 years. - .PARAMETER CertRotateConfig - The Config generated by New-SdnCertificateRotationConfig to include appropriate certificate thumbprints for mux nodes. .PARAMETER Force Switch to force the rotation without being prompted, when Service Fabric is unhealthy. #> [CmdletBinding(DefaultParameterSetName = 'GenerateCertificate')] param ( - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] [System.String]$NetworkController, - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, - [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] - [System.String]$CertPath, - [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] [Switch]$GenerateCertificate, - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] [System.Security.SecureString]$CertPassword, [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] [datetime]$NotAfter = (Get-Date).AddYears(3), - [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] - [hashtable]$CertRotateConfig, - - [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] [switch]$Force ) - # these are not yet supported and will take a bit more time to implement as it touches on core framework for rotate functionality - # however majority of the environments impacted are using sdnexpress which leverage self-signed certificates. - if ($CertRotateConfig -or $CertPath) { - "This feature is not yet supported and is under development. Please use -GenerateCertificate or reference {0} for manual steps." ` - -f 'https://learn.microsoft.com/en-us/azure-stack/hci/manage/update-network-controller-certificates?tabs=manual-renewal' | Trace-Output -Level:Warning - return - } - # ensure that the module is running as local administrator - $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-NOT $elevated) { - throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.") - } - - # add disclaimer that this feature is currently under preview - if (!$Force) { - "This feature is currently under preview. Please report any issues to https://github.com/microsoft/SdnDiagnostics/issues so we can accurately track any issues and help unblock your cert rotation." | Trace-Output -Level:Warning - $confirm = Confirm-UserInput -Message "Do you want to proceed with certificate rotation? [Y/N]:" - if (-NOT $confirm) { - "User has opted to abort the operation. Terminating operation" | Trace-Output -Level:Warning - return - } - } + Confirm-IsAdmin $array = @() $headers = @{"Accept"="application/json"} diff --git a/src/modules/SdnDiag.NetworkController/SdnDiag.NetworkController.Helper.psm1 b/src/modules/SdnDiag.NetworkController/SdnDiag.NetworkController.Helper.psm1 index 24a9bceb..8ec58cdf 100644 --- a/src/modules/SdnDiag.NetworkController/SdnDiag.NetworkController.Helper.psm1 +++ b/src/modules/SdnDiag.NetworkController/SdnDiag.NetworkController.Helper.psm1 @@ -10,6 +10,52 @@ class SdnFabricInfrastructure { [System.String[]]$FabricNodes } +class BaseCert { + [String]$Thumbprint + [String]$SubjectName + [bool]$IsSelfSigned +} + +class RestCertificate : BaseCert { + [CertType]$CertificateType = [CertType]::RestCertificate +} + +class NodeCert : BaseCert { + [String]$ResourceRef + [String]$IpAddressOrFQDN + [String]$NodeName +} + +class NetworkControllerNodeCert : NodeCert { + [CertType]$CertificateType = [CertType]::NetworkControllerNodeCert +} + +class LoadBalancerMuxNodeCert : NodeCert { + [CertType]$CertificateType = [CertType]::LoadBalancerMuxNodeCert +} + +class ServerNodeCert : NodeCert { + [CertType]$CertificateType = [CertType]::ServerNodeCert +} + +class CertRotateConfig { + [RestCertificate]$RestCertificate + [ClusterCredentialType]$ClusterCredentialType = [ClusterCredentialType]::Kerberos + [Object[]]$NodeCerts +} + +enum ClusterCredentialType { + Kerberos + X509 +} + +enum CertType { + RestCertificate + NetworkControllerNodeCert + ServerNodeCert + LoadBalancerMuxNodeCert +} + enum NcManagedRoles { Gateway Server diff --git a/src/modules/SdnDiag.NetworkController/private/Confirm-IsNetworkController.ps1 b/src/modules/SdnDiag.NetworkController/private/Confirm-IsNetworkController.ps1 new file mode 100644 index 00000000..e67233d7 --- /dev/null +++ b/src/modules/SdnDiag.NetworkController/private/Confirm-IsNetworkController.ps1 @@ -0,0 +1,7 @@ +function Confirm-IsNetworkController { + $config = Get-SdnModuleConfiguration -Role 'NetworkController' + $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.windowsFeature + if (-NOT ($confirmFeatures)) { + throw New-Object System.NotSupportedException("The current machine is not a NetworkController, run this on NetworkController.") + } +} diff --git a/src/modules/SdnDiag.NetworkController/private/Start-SdnExpiredCertificateRotation.ps1 b/src/modules/SdnDiag.NetworkController/private/Start-SdnExpiredCertificateRotation.ps1 index f9d5d1a7..d934165d 100644 --- a/src/modules/SdnDiag.NetworkController/private/Start-SdnExpiredCertificateRotation.ps1 +++ b/src/modules/SdnDiag.NetworkController/private/Start-SdnExpiredCertificateRotation.ps1 @@ -22,11 +22,12 @@ function Start-SdnExpiredCertificateRotation { param ( [Parameter(Mandatory = $true)] - [hashtable] - $CertRotateConfig, + [CertRotateConfig]$CertRotateConfig, + [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, + [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty @@ -44,41 +45,54 @@ function Start-SdnExpiredCertificateRotation { $NcNodeList = $NcInfraInfo.NodeList if ($null -eq $NcNodeList -or $NcNodeList.Count -eq 0) { - Trace-Output -Message "Failed to get NC Node List from NetworkController: $(HostName)" -Level:Error + throw "Failed to get NC Node List from NetworkController: $($env:COMPUTERNAME)" } Trace-Output -Message "NcNodeList: $($NcNodeList.IpAddressOrFQDN)" - Trace-Output -Message "Validate CertRotateConfig" if(!(Test-SdnCertificateRotationConfig -NcNodeList $NcNodeList -CertRotateConfig $CertRotateConfig -Credential $Credential)){ - Trace-Output -Message "Invalid CertRotateConfig, please correct the configuration and try again" -Level:Error - return + throw "Invalid CertRotateConfig, please correct the configuration and try again" } if ([String]::IsNullOrEmpty($NcInfraInfo.NcRestName)) { Trace-Output -Message "Failed to get NcRestName using current secret certificate thumbprint. This might indicate the certificate not found on $(HOSTNAME). We won't be able to recover." -Level:Error - throw New-Object System.NotSupportedException("Current NC Rest Cert not found, Certificate Rotation cannot be continue.") + throw New-Object System.NotSupportedException("Current Network Controller Rest certificate not found.") } - $NcVms = $NcNodeList.IpAddressOrFQDN - - if (Test-Path $NcUpdateFolder) { - $items = Get-ChildItem $NcUpdateFolder - if ($items.Count -gt 0) { - $confirmCleanup = Read-Host "The Folder $NcUpdateFolder not empty. Need to be cleared. Enter Y to confirm" - if ($confirmCleanup -eq "Y") { - $items | Remove-Item -Force -Recurse + if (Test-Path -Path $NcUpdateFolder -ItemType Container) { + $items = Get-ChildItem -Path $NcUpdateFolder + if ($items) { + $confirm = Confirm-UserInput -Message "$NcUpdateFolder not empty. Proceed with cleanup? [Y/N]: " + if (-NOT $confirm) { + # throw terminating exception here + # this will stop the execution Start-SdnCertificateRotation which calls into this function + throw New-Object System.OperationCanceledException("User cancelled the operation") } else { - return + $items | Remove-Item -Force -Recurse } } } - foreach ($nc in $NcVms) { - Invoke-Command -ComputerName $nc -ScriptBlock { - Write-Host "[$(HostName)] Stopping Service Fabric Service" - Stop-Service FabricHostSvc -Force + # stop service fabric service + $stopSfService = Invoke-PSRemoteCommand -ComputerName $NcNodeList.IpAddressOrFQDN -Credential $Credential -ScriptBlock { + Stop-Service -Name 'FabricHostSvc' -Force -ErrorAction Ignore 3>$null # redirect warning to null + if ((Get-Service -Name 'FabricHostSvc' -ErrorAction Ignore).Status -eq 'Stopped') { + return $true + } + else { + return $false + } + } -AsJob -PassThru -Activity 'Stopping Service Fabric Service on Network Controller' -ExecutionTimeOut 900 + + # enumerate the results of stopping service fabric service + # if any of the service fabric service is not stopped, throw an exception as we do not want to proceed further + $stopSfService | ForEach-Object { + if ($_) { + "Service Fabric Service stopped on {0}" -f $_.PSComputerName | Trace-Output + } + else { + throw "Failed to stop Service Fabric Service on $($_.PSComputerName)" } } @@ -93,16 +107,14 @@ function Start-SdnExpiredCertificateRotation { Trace-Output -Message "Step 3 Copy the new files back to the NC vms" Copy-ServiceFabricManifestToNetworkController -NcNodeList $NcNodeList -ManifestFolder $ManifestFolderNew -Credential $Credential - # Step 5 Start FabricHostSvc and wait for SF system service to become healty + # Step 4 Start FabricHostSvc and wait for SF system service to become healty Trace-Output -Message "Step 4 Start FabricHostSvc and wait for SF system service to become healty" - Trace-Output -Message "Step 4.1 Update Network Controller Certificate ACL to allow 'Network Service' Access" - Update-NetworkControllerCertificateAcl -NcNodeList $NcNodeList -CertRotateConfig $CertRotateConfig -Credential $Credential - Trace-Output -Message "Step 4.2 Start Service Fabric Host Service and wait" $clusterHealthy = Wait-ServiceFabricClusterHealthy -NcNodeList $NcNodeList -CertRotateConfig $CertRotateConfig -Credential $Credential Trace-Output -Message "ClusterHealthy: $clusterHealthy" if($clusterHealthy -ne $true){ throw New-Object System.NotSupportedException("Cluster unheathy after manifest update, we cannot continue with current situation") } + # Step 6 Invoke SF Cluster Upgrade Trace-Output -Message "Step 5 Invoke SF Cluster Upgrade" Update-ServiceFabricCluster -NcNodeList $NcNodeList -CertRotateConfig $CertRotateConfig -ManifestFolderNew $ManifestFolderNew -Credential $Credential @@ -122,15 +134,4 @@ function Start-SdnExpiredCertificateRotation { # Step 7 Restart Trace-Output -Message "Step 7 Restarting Service Fabric Cluster after configuration change" $clusterHealthy = Wait-ServiceFabricClusterHealthy -NcNodeList $NcNodeList -CertRotateConfig $CertRotateConfig -Credential $Credential -Restart - -<# Trace-Output -Message "Step 7.2 Rotate Network Controller Certificate" - #$null = Invoke-CertRotateCommand -Command 'Set-NetworkController' -Credential $Credential -Thumbprint $NcRestCertThumbprint - - # Step 8 Update REST CERT credential - Trace-Output -Message "Step 8 Update REST CERT credential" - # Step 8.1 Wait for NC App Healthy - Trace-Output -Message "Step 8.1 Wiating for Network Controller App Ready" - #Wait-NetworkControllerAppHealthy -Interval 60 - Trace-Output -Message "Step 8.2 Updating REST CERT Credential object calling REST API" #> - #Update-NetworkControllerCredentialResource -NcUri "https://$($NcInfraInfo.NcRestName)" -NewRestCertThumbprint $NcRestCertThumbprint -Credential $NcRestCredential } diff --git a/src/modules/SdnDiag.NetworkController/private/Test-NetworkControllerIsHealthy.ps1 b/src/modules/SdnDiag.NetworkController/private/Test-NetworkControllerIsHealthy.ps1 new file mode 100644 index 00000000..dc129d60 --- /dev/null +++ b/src/modules/SdnDiag.NetworkController/private/Test-NetworkControllerIsHealthy.ps1 @@ -0,0 +1,9 @@ +function Test-NetworkControllerIsHealthy { + try { + $null = Get-NetworkController -ErrorAction 'Stop' + return $true + } + catch { + return $false + } +} diff --git a/src/modules/SdnDiag.NetworkController/private/Update-NetworkControllerCertificateAcl.ps1 b/src/modules/SdnDiag.NetworkController/private/Update-NetworkControllerCertificateAcl.ps1 deleted file mode 100644 index 87f1d4ff..00000000 --- a/src/modules/SdnDiag.NetworkController/private/Update-NetworkControllerCertificateAcl.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -function Update-NetworkControllerCertificateAcl { - <# - .SYNOPSIS - Update the Network Controller Certificate to grant Network Service account read access to the private key. - .PARAMETER NcNodeList - The NcNodeList that retrieved via Get-SdnNetworkControllerInfoOffline. - .PARAMETER CertRotateConfig - The Config generated by New-SdnCertificateRotationConfig to include NC REST certificate thumbprint and node certificate thumbprint. - .PARAMETER Credential - Specifies a user account that has permission to perform this action. The default is the current user. - #> - - param ( - [Parameter(Mandatory = $true)] - [PSCustomObject[]] - $NcNodeList, - [Parameter(Mandatory = $true)] - [hashtable] - $CertRotateConfig, - [Parameter(Mandatory = $false)] - [System.Management.Automation.PSCredential] - [System.Management.Automation.Credential()] - $Credential = [System.Management.Automation.PSCredential]::Empty - ) - - try { - $NcRestCertThumbprint = $CertRotateConfig["NcRestCert"] - - foreach ($ncNode in $NcNodeList) { - $ncNodeCertThumbprint = $CertRotateConfig[$ncNode.NodeName.ToLower()] - Invoke-PSRemoteCommand -ComputerName $ncNode.IpAddressOrFQDN -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][String]$param2) - Set-SdnCertificateAcl -Path $param1 -Thumbprint $param2 - } -ArgumentList @('Cert:\LocalMachine\My', $NcRestCertThumbprint) - - if ($CertRotateConfig["ClusterCredentialType"] -ieq "X509") { - Invoke-PSRemoteCommand -ComputerName $ncNode.IpAddressOrFQDN -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][String]$param2) - Set-SdnCertificateAcl -Path $param1 -Thumbprint $param2 - } -ArgumentList @('Cert:\LocalMachine\My', $ncNodeCertThumbprint) - } - } - } - catch { - $_ | Trace-Exception - $_ | Write-Error - } -} diff --git a/src/modules/SdnDiag.NetworkController/private/Wait-ServiceFabricClusterHealthy.ps1 b/src/modules/SdnDiag.NetworkController/private/Wait-ServiceFabricClusterHealthy.ps1 index 44cb6dbe..d83ae0ba 100644 --- a/src/modules/SdnDiag.NetworkController/private/Wait-ServiceFabricClusterHealthy.ps1 +++ b/src/modules/SdnDiag.NetworkController/private/Wait-ServiceFabricClusterHealthy.ps1 @@ -13,12 +13,10 @@ function Wait-ServiceFabricClusterHealthy { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [PSCustomObject[]] - $NcNodeList, + [PSCustomObject[]]$NcNodeList, [Parameter(Mandatory = $true)] - [hashtable] - $CertRotateConfig, + [CertRotateConfig]$CertRotateConfig, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] @@ -26,8 +24,7 @@ function Wait-ServiceFabricClusterHealthy { $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] - [switch] - $Restart + [switch]$Restart ) try { @@ -35,7 +32,7 @@ function Wait-ServiceFabricClusterHealthy { # Start Service Fabric Service for each NC foreach ($ncNode in $NcNodeList) { - if(Test-ComputerNameIsLocal -ComputerName $ncNode.IpAddressOrFQDN){ + if (Test-ComputerNameIsLocal -ComputerName $ncNode.IpAddressOrFQDN) { $currentNcNode = $ncNode } @@ -59,10 +56,10 @@ function Wait-ServiceFabricClusterHealthy { $maxRetry = 10 $clusterConnected = $false while ($maxRetry -gt 0) { - if(!$clusterConnected){ - try{ + if (!$clusterConnected) { + try { "Service fabric cluster connect attempt $(11 - $maxRetry)/10" | Trace-Output - if ($CertRotateConfig["ClusterCredentialType"] -ieq "X509") { + if ($CertRotateConfig.ClusterCredentialType -ieq "X509") { "Connecting to Service Fabric Cluster using cert with thumbprint: {0}" -f $certThumb | Trace-Output Connect-ServiceFabricCluster -X509Credential -FindType FindByThumbprint -FindValue $certThumb -ConnectionEndpoint "$($NodeFQDN):49006" | Out-Null } @@ -70,13 +67,14 @@ function Wait-ServiceFabricClusterHealthy { Connect-ServiceFabricCluster | Out-Null } $clusterConnected = $true - }catch{ + } + catch { $maxRetry -- continue } } - if($clusterConnected){ + if ($clusterConnected) { $services = @() $services = Get-ServiceFabricService -ApplicationName fabric:/System $allServiceHealth = $true diff --git a/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNode.ps1 b/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNode.ps1 index bbcfd1c5..2d7c6d33 100644 --- a/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNode.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNode.ps1 @@ -6,7 +6,7 @@ function Get-SdnNetworkControllerNode { Specifies the friendly name of the node for the network controller. If not provided, settings are retrieved for all nodes in the deployment. .PARAMETER NetworkController Specifies the name or IP address of the network controller node on which this cmdlet operates. The parameter is optional if running on network controller node. - .PARAMETER Credential + .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. .EXAMPLE PS> Get-SdnNetworkControllerNode @@ -72,17 +72,13 @@ function Get-SdnNetworkControllerNode { # in this scenario if the results returned we will parse the objects returned and generate warning to user if node is not up # this property is only going to exist though if service fabric is healthy and underlying NC cmdlet can query node status - foreach($obj in $result){ - if($obj.Status -ine 'Up'){ - "{0} is reporting status {1}" -f $obj.Name, $obj.Status | Trace-Output -Level:Warning + $result | ForEach-Object { + if ($_.Status -ine 'Up') { + "{0} is reporting status {1}" -f $_.Name, $_.Status | Trace-Output -Level:Warning } - # if we returned the object, we want to add a new property called NodeCertificateThumbprint as this will ensure consistent - # output in scenarios where this operation fails due to NC unhealthy and we need to fallback to reading the cluster manifest - $result | ForEach-Object { - if (!($_.PSOBject.Properties.name -contains "NodeCertificateThumbprint")) { - $_ | Add-Member -MemberType NoteProperty -Name 'NodeCertificateThumbprint' -Value $_.NodeCertificate.Thumbprint - } + if (!($_.PSOBject.Properties.name -contains "NodeCertificateThumbprint")) { + $_ | Add-Member -MemberType NoteProperty -Name 'NodeCertificateThumbprint' -Value $_.NodeCertificate.Thumbprint } } } diff --git a/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNodeCertificate.ps1 b/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNodeCertificate.ps1 index 00589af3..6fd9cef7 100644 --- a/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNodeCertificate.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerNodeCertificate.ps1 @@ -26,17 +26,17 @@ function Get-SdnNetworkControllerNodeCertificate { switch ($networkControllerNode.FindCertificateBy) { 'FindBySubjectName' { "`tFindBySubjectName: {0}" -f $networkControllerNode.NodeCertSubjectName | Trace-Output -Level:Verbose - $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Subject $networkControllerNode.NodeCertSubjectName + $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Subject $networkControllerNode.NodeCertSubjectName -NetworkControllerOid:$NetworkControllerOid } 'FindByThumbprint' { "`FindByThumbprint: {0}" -f $networkControllerNode.NodeCertificateThumbprint | Trace-Output -Level:Verbose - $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Thumbprint $networkControllerNode.NodeCertificateThumbprint + $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Thumbprint $networkControllerNode.NodeCertificateThumbprint -NetworkControllerOid:$NetworkControllerOid } } } else { - $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Thumbprint $networkControllerNode.NodeCertificateThumbprint + $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Thumbprint $networkControllerNode.NodeCertificateThumbprint -NetworkControllerOid:$NetworkControllerOid } if ($null -eq $certificate) { diff --git a/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerRestCertificate.ps1 b/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerRestCertificate.ps1 index eb283b91..3af583b5 100644 --- a/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerRestCertificate.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/Get-SdnNetworkControllerRestCertificate.ps1 @@ -19,16 +19,16 @@ function Get-SdnNetworkControllerRestCertificate { try { $networkController = Get-SdnNetworkController -NetworkController $env:COMPUTERNAME -Credential $Credential $ncRestCertThumprint = $($networkController.ServerCertificate.Thumbprint).ToString() - $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Thumbprint $ncRestCertThumprint - - if ($null -eq $certificate) { - throw New-Object System.NullReferenceException("Unable to locate Network Controller Rest Certificate") - } - - return $certificate + $certificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Thumbprint $ncRestCertThumprint -ErrorAction 'Stop' } catch { $_ | Trace-Exception $_ | Write-Error } + + if ($null -eq $certificate) { + throw New-Object System.NullReferenceException("Unable to locate Network Controller Rest Certificate") + } + + return $certificate } diff --git a/src/modules/SdnDiag.NetworkController/public/New-SdnCertificateRotationConfig.ps1 b/src/modules/SdnDiag.NetworkController/public/New-SdnCertificateRotationConfig.ps1 index 8c6d1efa..62da82f1 100644 --- a/src/modules/SdnDiag.NetworkController/public/New-SdnCertificateRotationConfig.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/New-SdnCertificateRotationConfig.ps1 @@ -1,69 +1,112 @@ function New-SdnCertificateRotationConfig { <# .SYNOPSIS - Prepare the Network Controller Ceritifcate Rotation Configuration to determine which certificates to be used. - .PARAMETER NetworkController - Specifies the name the network controller node on which this cmdlet operates. The parameter is optional if running on network controller node. + Prepare the Network Controller Certificate Rotation Configuration to determine which certificates to be used. .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. .EXAMPLE - PS> New-SdnCertificateRotationConfig - .EXAMPLE - PS> New-SdnCertificateRotationConfig -NetworkController 'NC01' -Credential (Get-Credential) + PS> New-SdnCertificateRotationConfig -CertificateType 'Rest' #> [CmdletBinding()] param ( + [Parameter(Mandatory = $true)] + [CertType]$CertificateType, + [Parameter(Mandatory = $false)] - [String]$NetworkController = $(HostName), + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] - $Credential = [System.Management.Automation.PSCredential]::Empty + $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $false)] + [switch]$NetworkControllerOid ) + $certRotateConfig = [CertRotateConfig]::new() + try { - if (-NOT ($PSBoundParameters.ContainsKey('NetworkController'))) { - $config = Get-SdnModuleConfiguration -Role 'NetworkController' - $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.windowsFeature - if (-NOT ($confirmFeatures)) { - "The current machine is not a NetworkController, run this on NetworkController or use -NetworkController parameter to specify one" | Trace-Output -Level:Warning - return # don't throw exception, since this is a controlled scenario and we do not need stack exception tracing + $ncInfraInfo = Get-SdnNetworkControllerInfoOffline + $certRotateConfig.ClusterCredentialType = $ncInfraInfo.ClusterCredentialType + [string]$restSubjectName = "CN=$($NcInfraInfo.NcRestName)" + [uri]$ncUrl = "https://$($NcInfraInfo.NcRestName)" + + switch ($CertificateType) { + 'LoadBalancerMuxNodeCert' { + $servers = Get-SdnLoadBalancerMux -NcUri $ncUrl -Credential $NcRestCredential + $servers | ForEach-Object { + $virtualServer = Get-SdnResource -NcUri $ncUrl -ResourceRef $_.properties.virtualServer.resourceRef + $connection = $virtualServer.properties.connections | Where-Object { $_.credentialType -ieq "X509Certificate" -or $_.credentialType -ieq "X509CertificateSubjectName" } + $managementAddress = $connection.managementAddresses[0] + + "Retrieving latest certificate from $managementAddress" | Trace-Output + $cert = Invoke-PSRemoteCommand -ComputerName $managementAddress -ScriptBlock { + param([switch]$arg0) + return (Get-SdnMuxCertificate -NetworkControllerOid:$arg0) + } -ArgumentList @($NetworkControllerOid) -Credential $Credential -ErrorAction Stop + + $newestCert = $cert | Sort-Object -Property NotAfter -Descending | Select-Object -First 1 + $certRotateConfig.NodeCerts += [LoadBalancerMuxNodeCert]@{ + Thumbprint = $newestCert.Thumbprint + SubjectName = $newestCert.Subject + IpAddressOrFQDN = $managementAddress + NodeName = $newestCert.PSComputerName + ResourceRef = $_.ResourceRef + IsSelfSigned = (Confirm-IsCertSelfSigned -Certificate $newestCert) + } + } } - } - $NcInfraInfo = Get-SdnNetworkControllerInfoOffline -NetworkController $NetworkController -Credential $Credential + 'NetworkControllerNodeCert' { + } - $CertificateRotationConfig = @{} - $CertificateRotationConfig["ClusterCredentialType"] = $NcInfraInfo.ClusterCredentialType - $getNewestCertScript = { - param( - [String] - $certSubject - ) + 'RestCertificate' { + # grab the rest certificate with the latest expiration date + $restCertificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Subject $restSubjectName -NetworkControllerOid:$NetworkControllerOid ` + | Sort-Object -Property NotAfter -Descending | Select-Object -First 1 - # Default to return Node Certificate - if ([string]::IsNullOrEmpty($certSubject)) { - $NodeFQDN = (get-ciminstance win32_computersystem).DNSHostName + "." + (get-ciminstance win32_computersystem).Domain - $certSubject = "CN=$NodeFQDN" + if ($null -eq $restCertificate) { + throw New-Object System.NullReferenceException("Failed to locate Rest certificate") + } + + $CertRotateConfig.RestCertificate = [RestCertificate]@{ + CertificateType = 'Rest' + Thumbprint = $restCertificate.Thumbprint + SubjectName = $restCertificate.Subject + IsSelfSigned = (Confirm-IsCertSelfSigned -Certificate $restCertificate) + } } - Write-Verbose "Looking for cert match $certSubject" - $cert = Get-ChildItem -Path Cert:\LocalMachine\My | ? { $_.Subject -ieq $certSubject } | Sort-Object -Property NotBefore -Descending | Select-Object -First 1 - return $cert.Thumbprint - } - $CertificateRotationConfig["NcRestCert"] = Invoke-PSRemoteCommand -ComputerName $NetworkController -ScriptBlock $getNewestCertScript -ArgumentList "CN=$($NcInfraInfo.NcRestName)" -Credential $Credential + 'ServerNodeCert' { + $servers = Get-SdnServer -NcUri $ncUrl -Credential $NcRestCredential + $servers | ForEach-Object { + $connection = $_.properties.connections | Where-Object { $_.credentialType -ieq "X509Certificate" -or $_.credentialType -ieq "X509CertificateSubjectName" } + $managementAddress = $connection.managementAddresses[0] + + "Retrieving latest certificate from $managementAddress" | Trace-Output + $cert = Invoke-PSRemoteCommand -ComputerName $managementAddress -ScriptBlock { + param([switch]$arg0) + return (Get-SdnServerCertificate -NetworkControllerOid:$arg0) + } -ArgumentList @($NetworkControllerOid) -Credential $Credential -ErrorAction Stop - if($NcInfraInfo.ClusterCredentialType -eq "X509"){ - foreach ($ncNode in $($NcInfraInfo.NodeList)) { - Trace-Output -Message "Looking for Node Cert for Node: $($ncNode.NodeName), IpAddressOrFQDN: $($ncNode.IpAddressOrFQDN)" -Level:Verbose - $ncNodeCert = Invoke-PSRemoteCommand -ComputerName $ncNode.IpAddressOrFQDN -ScriptBlock $getNewestCertScript -Credential $Credential - $CertificateRotationConfig[$ncNode.NodeName.ToLower()] = $ncNodeCert + $newestCert = $cert | Sort-Object -Property NotAfter -Descending | Select-Object -First 1 + $certRotateConfig.NodeCerts += [ServerNodeCert]@{ + Thumbprint = $newestCert.Thumbprint + SubjectName = $newestCert.Subject + IpAddressOrFQDN = $managementAddress + NodeName = $newestCert.PSComputerName + ResourceRef = $_.ResourceRef + IsSelfSigned = (Confirm-IsCertSelfSigned -Certificate $newestCert) + } + } } } - return $CertificateRotationConfig + return $certRotateConfig } catch { $_ | Trace-Exception diff --git a/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerNodeCertificate.ps1 b/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerNodeCertificate.ps1 index aebebc10..bf37e2bf 100644 --- a/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerNodeCertificate.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerNodeCertificate.ps1 @@ -19,7 +19,7 @@ function New-SdnNetworkControllerNodeCertificate { [System.Security.SecureString]$CertPassword, [Parameter(Mandatory = $false)] - [System.String]$Path = "$(Get-WorkingDirectory)\Cert_{0}" -f (Get-FormattedDateTimeUTC), + [System.String]$Path = "$(Get-WorkingDirectory)\NcCert_{0}" -f (Get-FormattedDateTimeUTC), [Parameter(Mandatory = $false)] [System.Object]$FabricDetails, @@ -50,18 +50,18 @@ function New-SdnNetworkControllerNodeCertificate { if (-NOT (Test-Path -Path $Path -PathType Container)) { "Creating directory {0}" -f $Path | Trace-Output - $CertPath = New-Item -Path $Path -ItemType Directory -Force + $certPath = New-Item -Path $Path -ItemType Directory -Force } else { - $CertPath = Get-Item -Path $Path + $certPath = Get-Item -Path $Path } $nodeCertSubject = (Get-SdnNetworkControllerNodeCertificate).Subject - $certificate = New-SdnCertificate -Subject $nodeCertSubject -NotAfter $NotAfter + $certificate = New-SdnSelfSignedCertificate -Subject $nodeCertSubject -NotAfter $NotAfter # after the certificate has been generated, we want to export the certificate using the $CertPassword provided by the operator # and save the file to directory. This allows the rest of the function to pick up these files and perform the steps as normal - [System.String]$pfxFilePath = "$(Join-Path -Path $CertPath.FullName -ChildPath $nodeCertSubject.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).pfx" + [System.String]$pfxFilePath = "$(Join-Path -Path $certPath.FullName -ChildPath $nodeCertSubject.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).pfx" "Exporting pfx certificate to {0}" -f $pfxFilePath | Trace-Output $exportedCertificate = Export-PfxCertificate -Cert $certificate -FilePath $pfxFilePath -Password $CertPassword -CryptoAlgorithmOption AES256_SHA256 $null = Import-SdnCertificate -FilePath $exportedCertificate.FullName -CertStore 'Cert:\LocalMachine\Root' -CertPassword $CertPassword @@ -78,4 +78,6 @@ function New-SdnNetworkControllerNodeCertificate { $_ | Trace-Exception $_ | Write-Error } + + return $null } diff --git a/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerRestCertificate.ps1 b/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerRestCertificate.ps1 index 974e894c..c10053f7 100644 --- a/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerRestCertificate.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/New-SdnNetworkControllerRestCertificate.ps1 @@ -5,7 +5,7 @@ function New-SdnNetworkControllerRestCertificate { .PARAMETER NotAfter Specifies the date and time, as a DateTime object, that the certificate expires. To obtain a DateTime object, use the Get-Date cmdlet. The default value for this parameter is one year after the certificate was created. .PARAMETER CertPassword - Specifies the password for the imported PFX file in the form of a secure string. + Specifies the password for the PFX file in the form of a secure string. #> [CmdletBinding()] @@ -20,10 +20,7 @@ function New-SdnNetworkControllerRestCertificate { [System.Security.SecureString]$CertPassword, [Parameter(Mandatory = $false)] - [System.String]$Path = "$(Get-WorkingDirectory)\Cert_{0}" -f (Get-FormattedDateTimeUTC), - - [Parameter(Mandatory = $false)] - [System.Object]$FabricDetails, + [System.String]$Path = "$(Get-WorkingDirectory)\NcRest_{0}" -f (Get-FormattedDateTimeUTC), [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] @@ -37,53 +34,27 @@ function New-SdnNetworkControllerRestCertificate { } # ensure that the module is running as local administrator - $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-NOT $elevated) { - throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.") - } + Confirm-IsAdmin try { - if ($FabricDetails) { - if ($FabricDetails.LoadBalancerMux -or $FabricDetails.Server) { - $installToSouthboundDevices = $true - } - else { - $installToSouthboundDevices = $false - } - } - else { - $installToSouthboundDevices = $false - - $FabricDetails = [SdnFabricInfrastructure]@{ - NetworkController = (Get-SdnNetworkControllerNode).Server - } - } - if (-NOT (Test-Path -Path $Path -PathType Container)) { "Creating directory {0}" -f $Path | Trace-Output - $CertPath = New-Item -Path $Path -ItemType Directory -Force + $certPath = New-Item -Path $Path -ItemType Directory -Force } else { - $CertPath = Get-Item -Path $Path + $certPath = Get-Item -Path $Path } [System.String]$formattedSubject = "CN={0}" -f $RestName.Trim() - $certificate = New-SdnCertificate -Subject $formattedSubject -NotAfter $NotAfter + $certificate = New-SdnSelfSignedCertificate -Subject $formattedSubject -NotAfter $NotAfter # after the certificate has been generated, we want to export the certificate using the $CertPassword provided by the operator - # and save the file to directory. This allows the rest of the function to pick up these files and perform the steps as normal - [System.String]$pfxFilePath = "$(Join-Path -Path $CertPath.FullName -ChildPath $RestName.ToLower().Replace('.','_').Replace('=','_').Trim()).pfx" + [System.String]$pfxFilePath = "$(Join-Path -Path $certPath.FullName -ChildPath $RestName.ToLower().Replace('.','_').Replace('=','_').Trim()).pfx" "Exporting pfx certificate to {0}" -f $pfxFilePath | Trace-Output $exportedCertificate = Export-PfxCertificate -Cert $certificate -FilePath $pfxFilePath -Password $CertPassword -CryptoAlgorithmOption AES256_SHA256 $null = Import-SdnCertificate -FilePath $exportedCertificate.FullName -CertStore 'Cert:\LocalMachine\Root' -CertPassword $CertPassword - Copy-CertificateToFabric -CertFile $exportedCertificate.FullName -CertPassword $CertPassword -FabricDetails $FabricDetails ` - -NetworkControllerRestCertificate -InstallToSouthboundDevices:$installToSouthboundDevices -Credential $Credential - - return ([PSCustomObject]@{ - Certificate = $certificate - FileInfo = $exportedCertificate - }) + return $exportedCertificate } catch { $_ | Trace-Exception diff --git a/src/modules/SdnDiag.NetworkController/public/Start-SdnCertificateRotation.ps1 b/src/modules/SdnDiag.NetworkController/public/Start-SdnCertificateRotation.ps1 index c6aa3202..a33d2eaf 100644 --- a/src/modules/SdnDiag.NetworkController/public/Start-SdnCertificateRotation.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/Start-SdnCertificateRotation.ps1 @@ -22,6 +22,12 @@ function Start-SdnCertificateRotation { [CmdletBinding(DefaultParameterSetName = 'GenerateCertificate')] param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] + [ValidateSet('Rest','NetworkController','Server','LoadBalancerMux')] + [System.String]$CertificateType, + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] @@ -59,10 +65,7 @@ function Start-SdnCertificateRotation { ) # ensure that the module is running as local administrator - $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-NOT $elevated) { - throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.") - } + Confirm-IsAdmin $config = Get-SdnModuleConfiguration -Role 'NetworkController' $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.windowsFeature @@ -70,16 +73,6 @@ function Start-SdnCertificateRotation { throw New-Object System.NotSupportedException("The current machine is not a NetworkController, run this on NetworkController.") } - # add disclaimer that this feature is currently under preview - if (!$Force) { - "This feature is currently under preview. Please report any issues to https://github.com/microsoft/SdnDiagnostics/issues so we can accurately track any issues and help unblock your cert rotation." | Trace-Output -Level:Warning - $confirm = Confirm-UserInput -Message "Do you want to proceed with certificate rotation? [Y/N]:" - if (-NOT $confirm) { - "User has opted to abort the operation. Terminating operation" | Trace-Output -Level:Warning - return - } - } - try { "Starting certificate rotation" | Trace-Output @@ -177,26 +170,23 @@ function Start-SdnCertificateRotation { if ($PSCmdlet.ParameterSetName -ieq 'GenerateCertificate') { "== STAGE: CREATE SELF SIGNED CERTIFICATES ==" | Trace-Output - $newSelfSignedCert = New-SdnNetworkControllerRestCertificate -RestName $NcInfraInfo.NcRestName.ToString() -NotAfter $NotAfter -Path $CertPath.FullName ` - -CertPassword $CertPassword -Credential $Credential -FabricDetails $sdnFabricDetails - - $selfSignedRestCertFile = $newSelfSignedCert.FileInfo - - if ($rotateNCNodeCerts) { - $null = Invoke-PSRemoteCommand -ComputerName $sdnFabricDetails.NetworkController -Credential $Credential -ScriptBlock { - param( - [Parameter(Position = 0)][DateTime]$param1, - [Parameter(Position = 1)][SecureString]$param2, - [Parameter(Position = 2)][PSCredential]$param3, - [Parameter(Position = 3)][String]$param4, - [Parameter(Position = 4)][System.Object]$param5 - ) - - New-SdnNetworkControllerNodeCertificate -NotAfter $param1 -CertPassword $param2 -Credential $param3 -Path $param4 -FabricDetails $param5 - } -ArgumentList @($NotAfter, $CertPassword, $Credential, $CertPath.FullName, $sdnFabricDetails) + switch ($CertificateType) { + 'Rest' { + $newSelfSignedCert = New-SdnNetworkControllerRestCertificate -RestName $NcInfraInfo.NcRestName.ToString() -NotAfter $NotAfter -Path $CertPath.FullName ` + -CertPassword $CertPassword -Credential $Credential -FabricDetails $sdnFabricDetails + $selfSignedRestCertFile = $newSelfSignedCert.FileInfo + $encryptionCertificate = $newSelfSignedCert.FileInfo + } + 'NetworkController' { + $newSelfSignedCert = New-SdnNetworkControllerCertificate -NotAfter $NotAfter -Path $CertPath.FullName -CertPassword $CertPassword -Credential $Credential + } + 'Server' { + $newSelfSignedCert = New-SdnServerCertificate -NotAfter $NotAfter -Path $CertPath.FullName -CertPassword $CertPassword -Credential $Credential + } + 'LoadBalancerMux' { + $newSelfSignedCert = New-SdnLoadBalancerMuxCertificate -NotAfter $NotAfter -Path $CertPath.FullName -CertPassword $CertPassword -Credential $Credential + } } - - $CertRotateConfig = New-SdnCertificateRotationConfig -Credential $Credential } ##################################### @@ -236,31 +226,14 @@ function Start-SdnCertificateRotation { throw New-Object System.NotSupportedException("Unable to validate certificate configuration") } - $updatedRestCertificate = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Subject -ieq $currentRestCert.Subject } ` - | Sort-Object -Property NotBefore -Descending | Select-Object -First 1 + # grab the rest certificate with the latest expiration date + $updatedRestCertificate = Get-SdnCertificate -Path 'Cert:\LocalMachine\My' -Subject $currentRestCert.Subject -NetworkControllerOid ` + | Sort-Object -Property NotAfter -Descending | Select-Object -First 1 "Network Controller Rest Certificate {0} will be updated from [Thumbprint:{1} NotAfter:{2}] to [Thumbprint:{3} NotAfter:{4}]" ` -f $currentRestCert.Subject, $currentRestCert.Thumbprint, $currentRestCert.NotAfter, $CertRotateConfig["NcRestCert"], $updatedRestCertificate.NotAfter ` | Trace-Output -Level:Warning - if ($rotateNCNodeCerts) { - foreach ($node in $NcInfraInfo.NodeList) { - $nodeCertThumbprint = $certRotateConfig[$node.NodeName.ToLower()] - $currentNodeCert = Invoke-PSRemoteCommand -ComputerName $node.IpAddressOrFQDN -Credential $Credential -ScriptBlock { - Get-SdnNetworkControllerNodeCertificate - } - - $newNodeCert = Invoke-PSRemoteCommand -ComputerName $node.IpAddressOrFQDN -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][String]$param2) - Get-SdnCertificate -Path $param1 -Thumbprint $param2 - } -ArgumentList @('Cert:\LocalMachine\My', $nodeCertThumbprint) - - "Network Controller Node Certificate {0} will be updated from [Thumbprint:{1} NotAfter:{2}] to [Thumbprint:{3} NotAfter:{4}]" ` - -f $currentNodeCert.Subject, $currentNodeCert.Thumbprint, $currentNodeCert.NotAfter, ` - $newNodeCert.Thumbprint, $newNodeCert.NotAfter | Trace-Output -Level:Warning - } - } - if (!$Force) { $confirm = Confirm-UserInput if (-NOT $confirm) { @@ -271,129 +244,38 @@ function Start-SdnCertificateRotation { ##################################### # - # Rotate NC Certificate Expired + # Rotate Certificates # ##################################### - if ($restCertExpired -or !$ncHealthy) { - # Use this for certificate if either rest cert expired or nc unhealthy, get-networkcontroller failed - Start-SdnExpiredCertificateRotation -CertRotateConfig $CertRotateConfig -Credential $Credential -NcRestCredential $NcRestCredential - } - - ##################################### - # - # Rotate NC Northbound Certificate (REST) - # - ##################################### - - "== STAGE: ROTATE NC REST CERTIFICATE ==" | Trace-Output - - $null = Invoke-CertRotateCommand -Command 'Set-NetworkController' -Credential $Credential -Thumbprint $CertRotateConfig["NcRestCert"] - - "Waiting for 5 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output - Start-Sleep -Seconds 300 - - ##################################### - # - # Rotate Cluster Certificate - # - ##################################### - - "== STAGE: ROTATE NC CLUSTER CERTIFICATE ==" | Trace-Output - - $null = Invoke-CertRotateCommand -Command 'Set-NetworkControllerCluster' -Credential $Credential -Thumbprint $CertRotateConfig["NcRestCert"] - - "Waiting for 5 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output - Start-Sleep -Seconds 300 - - ##################################### - # - # Rotate NC Node Certificates - # - ##################################### - - if ($rotateNCNodeCerts) { - "== STAGE: ROTATE NC NODE CERTIFICATE ==" | Trace-Output - - foreach ($node in $NcInfraInfo.NodeList) { - $nodeCertThumbprint = $certRotateConfig[$node.NodeName.ToLower()] - $null = Invoke-CertRotateCommand -Command 'Set-NetworkControllerNode' -NetworkController $node.IpAddressOrFQDN -Name $node.NodeName -Credential $Credential -Thumbprint $nodeCertThumbprint - - "Waiting for 2 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output - Start-Sleep -Seconds 120 - } - } - - ##################################### - # - # Rotate NC Southbound Certificates - # - ##################################### - - "== STAGE: ROTATE SOUTHBOUND CERTIFICATE CREDENTIALS ==" | Trace-Output - - $null = Update-NetworkControllerCredentialResource -NcUri "https://$($NcInfraInfo.NcRestName)" -Credential $NcRestCredential ` - -NewRestCertThumbprint $CertRotateConfig["NcRestCert"] -ErrorAction Stop - - "Southbound certificate rotation completed" | Trace-Output - - ##################################### - # - # Certificate Seeding (Southbound Nodes) - # - ##################################### - - # if nc was unhealthy and unable to determine southbound devices in the dataplane earlier - # we now want to check to see if nc is healthy and if we need to install the rest cert (for self-signed) to southbound devices - if ($postRotateSBRestCert) { - if ($selfSignedRestCertFile) { - $sdnFabricDetails = Get-SdnInfrastructureInfo -Credential $Credential -NcRestCredential $NcRestCredential -Force - $southBoundNodes = @() - if ($null -ne $sdnFabricDetails.LoadBalancerMux) { - $southBoundNodes += $sdnFabricDetails.LoadBalancerMux + switch ($CertificateType) { + 'Rest' { + # if the rest certificate / encryption certificate is expired, we need to rotate it + if ($restCertExpired -or !$ncHealthy) { + Start-SdnExpiredCertificateRotation -CertRotateConfig $CertRotateConfig -Credential $Credential -NcRestCredential $NcRestCredential } - if ($null -ne $sdnFabricDetails.Server) { - $southBoundNodes += $sdnFabricDetails.Server - } - - if ($southBoundNodes) { - "== STAGE: REST SELF-SIGNED CERTIFICATE SEEDING (Southbound Nodes) ==" | Trace-Output - - # ensure that we have the latest version of sdnDiagnostics module on the southbound devices - Install-SdnDiagnostics -ComputerName $southBoundNodes -Credential $Credential -ErrorAction Stop - "[REST CERT] Installing self-signed certificate to {0}" -f ($southBoundNodes -join ', ') | Trace-Output - [System.String]$remoteFilePath = Join-Path -Path $CertPath.FullName -ChildPath $selfSignedRestCertFile.Name - Copy-FileToRemoteComputer -ComputerName $southBoundNodes -Credential $Credential -Path $selfSignedRestCertFile.FullName -Destination $remoteFilePath - $null = Invoke-PSRemoteCommand -ComputerName $southBoundNodes -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][String]$param2) - Import-SdnCertificate -FilePath $param1 -CertStore $param2 - } -ArgumentList @($remoteFilePath, 'Cert:\LocalMachine\Root') -ErrorAction Stop - } + # rotate the rest certificate + "== STAGE: ROTATE NC REST CERTIFICATE ==" | Trace-Output + $null = Invoke-CertRotateCommand -Command 'Set-NetworkController' -Credential $Credential -Thumbprint $CertRotateConfig["NcRestCert"] + "Waiting for 5 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output + Start-Sleep -Seconds 300 + + # rotate the encryption certificate + "== STAGE: ROTATE NC CLUSTER CERTIFICATE ==" | Trace-Output + $null = Invoke-CertRotateCommand -Command 'Set-NetworkControllerCluster' -Credential $Credential -Thumbprint $CertRotateConfig["NcRestCert"] + "Waiting for 5 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output + Start-Sleep -Seconds 300 } } ##################################### # - # Restart services + # Certificate Seeding (Southbound Nodes) # ##################################### - "== STAGE: RESTART NETWORK CONTROLLER SERVICES ==" | Trace-Output - # restart the network controller services - # this will force new TLS connections to be established to southbound devices - # ensuring that the new certificates are used and we are able to push policies successfully - - # check to determine if we have a multi-node NC cluster and if so, leverage the SF cmdlets to move the replicas - # otherwise, we will just stop the processes and let SF restart them automatically - if ($sdnFabricDetails.NetworkController.Count -gt 1) { - Move-SdnServiceFabricReplica -ServiceTypeName 'SlbManagerService' - Move-SdnServiceFabricReplica -ServiceTypeName 'VSwitchService' - } - else { - Get-Process -Name 'SDNFW' | Stop-Process -Force -ErrorAction Continue - Get-Process -Name 'SDNSLBM' | Stop-Process -Force -ErrorAction Continue - } + $rotate "Certificate rotation has completed" | Trace-Output } diff --git a/src/modules/SdnDiag.NetworkController/public/Start-SdnNetworkControllerCertificateRotation.ps1 b/src/modules/SdnDiag.NetworkController/public/Start-SdnNetworkControllerCertificateRotation.ps1 new file mode 100644 index 00000000..69125a95 --- /dev/null +++ b/src/modules/SdnDiag.NetworkController/public/Start-SdnNetworkControllerCertificateRotation.ps1 @@ -0,0 +1,136 @@ +function Start-SdnNetworkControllerCertificateRotation { + <# + .SYNOPSIS + Performs a rotate of the Network Controller node certificates if using X509. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .PARAMETER NcRestCredential + Specifies a user account that has permission to access the northbound NC API interface. The default is the current user. + .PARAMETER CertPath + Path directory where certificate(s) .pfx files are located for use with certificate rotation. + .PARAMETER GenerateCertificate + Switch to determine if certificate rotate function should generate self-signed certificates. + .PARAMETER CertPassword + SecureString password for accessing the .pfx files, or if using -GenerateCertificate, what the .pfx files will be encrypted with. + .PARAMETER NotAfter + Expiration date when using -GenerateCertificate. If ommited, defaults to 3 years. + .PARAMETER CertRotateConfig + The Config generated by New-SdnCertificateRotationConfig to include NC REST certificate thumbprint and node certificate thumbprint. + .PARAMETER Force + Switch to force the rotation without being prompted, when Service Fabric is unhealthy. + #> + + [CmdletBinding(DefaultParameterSetName = 'GenerateCertificate')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [System.String]$CertPath, + + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [Switch]$GenerateCertificate, + + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [System.Security.SecureString]$CertPassword, + + [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] + [datetime]$NotAfter = (Get-Date).AddYears(3), + + [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] + [hashtable]$CertRotateConfig, + + [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] + [switch]$Force + ) + + # ensure that the module is running as local administrator + Confirm-IsAdmin + + $config = Get-SdnModuleConfiguration -Role 'NetworkController' + $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.windowsFeature + if (-NOT ($confirmFeatures)) { + throw New-Object System.NotSupportedException("The current machine is not a NetworkController, run this on NetworkController.") + } + + # purge any existing remote sessions to prevent situation where + # we leverage a session without credentials + Remove-PSRemotingSession + + try { + "Starting Network Controller Node certificate rotation" | Trace-Output + + if ([String]::IsNullOrEmpty($CertPath)) { + [System.String]$CertPath = "$(Get-WorkingDirectory)\Cert_{0}" -f (Get-FormattedDateTimeUTC) + + if (-NOT (Test-Path -Path $CertPath -PathType Container)) { + $null = New-Item -Path $CertPath -ItemType Directory -Force + } + } + [System.IO.FileSystemInfo]$CertPath = Get-Item -Path $CertPath -ErrorAction Stop + + "Retrieving current SDN environment details" | Trace-Output + $ncInfrastructureInfo = Get-SdnNetworkControllerInfoOffline -Credential $Credential + if ($ncInfrastructureInfo.ClusterCredentialType -ine 'X509') { + "Network Controller nodes are not using X509 certificates and do not need to be rotated." | Trace-Output + return + } + + # before we proceed with anything else, we want to make sure that all the Network Controllers and MUXes within the SDN fabric are running the current version + Install-SdnDiagnostics -ComputerName $sdnFabricDetails.NetworkController -ErrorAction Stop + + $null = Invoke-PSRemoteCommand -ComputerName $sdnFabricDetails.NetworkController -Credential $Credential -ScriptBlock { + param( + [Parameter(Position = 0)][DateTime]$param1, + [Parameter(Position = 1)][SecureString]$param2, + [Parameter(Position = 2)][PSCredential]$param3, + [Parameter(Position = 3)][String]$param4, + [Parameter(Position = 4)][System.Object]$param5 + ) + + New-SdnNetworkControllerNodeCertificate -NotAfter $param1 -CertPassword $param2 -Credential $param3 -Path $param4 -FabricDetails $param5 + } -ArgumentList @($NotAfter, $CertPassword, $Credential, $CertPath.FullName, $sdnFabricDetails) + + foreach ($node in $NcInfraInfo.NodeList) { + $nodeCertThumbprint = $certRotateConfig[$node.NodeName.ToLower()] + $currentNodeCert = Invoke-PSRemoteCommand -ComputerName $node.IpAddressOrFQDN -Credential $Credential -ScriptBlock { + Get-SdnNetworkControllerNodeCertificate + } + + $newNodeCert = Invoke-PSRemoteCommand -ComputerName $node.IpAddressOrFQDN -Credential $Credential -ScriptBlock { + param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][String]$param2) + Get-SdnCertificate -Path $param1 -Thumbprint $param2 + } -ArgumentList @('Cert:\LocalMachine\My', $nodeCertThumbprint) + + "Network Controller Node Certificate {0} will be updated from [Thumbprint:{1} NotAfter:{2}] to [Thumbprint:{3} NotAfter:{4}]" ` + -f $currentNodeCert.Subject, $currentNodeCert.Thumbprint, $currentNodeCert.NotAfter, ` + $newNodeCert.Thumbprint, $newNodeCert.NotAfter | Trace-Output -Level:Warning + } + + foreach ($node in $NcInfraInfo.NodeList) { + $nodeCertThumbprint = $certRotateConfig[$node.NodeName.ToLower()] + $null = Invoke-CertRotateCommand -Command 'Set-NetworkControllerNode' -NetworkController $node.IpAddressOrFQDN -Credential $Credential -Thumbprint $nodeCertThumbprint + + "Waiting for 2 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output + Start-Sleep -Seconds 120 + } + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} diff --git a/src/modules/SdnDiag.NetworkController/public/Start-SdnNetworkControllerRestCertificateRotation.ps1 b/src/modules/SdnDiag.NetworkController/public/Start-SdnNetworkControllerRestCertificateRotation.ps1 new file mode 100644 index 00000000..43550cad --- /dev/null +++ b/src/modules/SdnDiag.NetworkController/public/Start-SdnNetworkControllerRestCertificateRotation.ps1 @@ -0,0 +1,285 @@ +function Start-SdnNetworkControllerRestCertificateRotation { + <# + .SYNOPSIS + Performs a rotate of the Network Controller Rest and Encryption certificate. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .PARAMETER NcRestCredential + Specifies a user account that has permission to access the northbound NC API interface. The default is the current user. + .PARAMETER Certificate + Specifies the certificate file path to be used for certificate rotation. + .PARAMETER GenerateCertificate + Switch to determine if certificate rotate function should generate self-signed certificates. + .PARAMETER CertPassword + SecureString password for accessing the .pfx files, or if using -GenerateCertificate, what the .pfx files will be encrypted with. + .PARAMETER NotAfter + Expiration date when using -GenerateCertificate. If ommited, defaults to 3 years. + .PARAMETER CertRotateConfig + The Config generated by New-SdnCertificateRotationConfig to include NC REST certificate thumbprint and node certificate thumbprint. + .PARAMETER Force + Switch to force the rotation without being prompted, when Service Fabric is unhealthy. + #> + + [CmdletBinding(DefaultParameterSetName = 'GenerateCertificate')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [System.String]$Certificate, + + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [Switch]$GenerateCertificate, + + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [System.Security.SecureString]$CertPassword, + + [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] + [CertRotateConfig]$CertRotateConfig, + + [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] + [datetime]$NotAfter = (Get-Date).AddYears(3), + + [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] + [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] + [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] + [switch]$Force + ) + + # ensure that the module is running as local administrator + # and that we are operating the cmdlet on a network controller + Confirm-IsAdmin + Confirm-IsNetworkController + + # if we are generating a certificate, we will want to create specify the certificate path + if ([String]::IsNullOrEmpty($CertPath)) { + [System.String]$CertPath = "$(Get-WorkingDirectory)\Cert_{0}" -f (Get-FormattedDateTimeUTC) + } + + try { + # create the directory if it does not exist + if (-NOT (Test-Path -Path $CertPath -PathType Container)) { + $null = New-Item -Path $CertPath -ItemType Directory -Force + } + $CertPathDir = Get-Item -Path $CertPath -ErrorAction 'Stop' + + "Retrieving current SDN environment details" | Trace-Output + $NcInfraInfo = Get-SdnNetworkControllerInfoOffline -Credential $Credential + + # get the network controller infrastructure information + # and determine if the rest certificate is expired or if the network controller is healthy + $currentRestCert = Get-SdnNetworkControllerRestCertificate -ErrorAction 'Stop' + $restCertExpired = (Get-Date) -gt $($currentRestCert.NotAfter) + if ($restCertExpired) { + "Network Controller Rest Certificate {0} expired at {1}" -f $currentRestCert.Thumbprint, $currentRestCert.NotAfter | Trace-Output -Level:Warning + $isNetworkControllerHealthy = $false + } + else { + $isNetworkControllerHealthy = Test-NetworkControllerIsHealthy + } + + if ($isNetworkControllerHealthy) { + $sdnFabricDetails = Get-SdnInfrastructureInfo -NetworkController $env:COMPUTERNAME -Credential $Credential -NcRestCredential $NcRestCredential + } + else { + $sdnFabricDetails = [SdnFabricInfrastructure]@{ + NetworkController = $NcInfraInfo.NodeList.IpAddressOrFQDN + } + } + + switch ($PSCmdlet.ParameterSetName) { + 'GenerateCertificate' { + "== STAGE: CREATE SELF SIGNED REST CERTIFICATE ==" | Trace-Output + $restCertFileParams = @{ + RestName = $NcInfraInfo.NcRestName + NotAfter = $NotAfter + Path = $CertPathDir.FullName + CertPassword = $CertPassword + Credential = $Credential + FabricDetails = $sdnFabricDetails + ErrorAction = 'Stop' + } + + New-SdnNetworkControllerRestCertificate @restCertFileParams + } + 'Pfx' { + "== STAGE: PARSE PFX CERTIFICATE ==" | Trace-Output + $pfxData = Get-PfxData -FilePath $Certificate -Password $CertPassword -ErrorAction 'Stop' + if ($pfxdata.EndEntityCertificates.Subject -ieq $currentRestCert.Subject) { + "Matched {0} [Subject: {1}; Thumbprint: {2}] to NC Rest Certificate" -f ` + $Certificate, $pfxData.EndEntityCertificates.Subject, $pfxData.EndEntityCertificates.Thumbprint | Trace-Output + } + $restCertFile = Get-Item -Path $Certificate -ErrorAction 'Stop' + } + 'CertConfig' { + "== STAGE: DETERMINE CERTIFICATE CONFIG ==" | Trace-Output + $certValidated = Test-SdnCertificateRotationConfig -NcNodeList $NcInfraInfo.NodeList -CertRotateConfig $CertRotateConfig -Credential $Credential + + if ($certValidated -ne $true) { + throw New-Object System.NotSupportedException("Unable to validate certificate configuration") + } + } + } + + # we will import the certificate, even though it may already exist + # as we want to ensure we return the properties that contain self-signed information + $importRestCert = Import-SdnCertificate -FilePath $restCertFile.FullName -CertStore 'Cert:\LocalMachine\My' -CertPassword $CertPassword -ErrorAction 'Stop' + + # in this instance, we will want to copy the certificate to the fabric + # however we will not copy to southbound devices at this time as we will do that later + # to account for situations where the current certificate is expired or network controller is unhealthy + # resulting in us not being able to determine the southbound devices + + "== STAGE: COPY CERTIFICATE TO FABRIC ==" | Trace-Output + $copyCertToFabricParams = @{ + CertFile = $restCertFile.FullName + CertPassword = $CertPassword + FabricDetails = $sdnFabricDetails + NetworkControllerRestCertificate = $true + InstallToSouthboundDevices = $false + Credential = $Credential + ErrorAction = 'Stop' + } + + Copy-CertificateToFabric @copyCertToFabricParams + + # generate the certificate rotation configuration + if (!$CertRotateConfig) { + $certRotateConfigParams = @{ + CertificateType = 'Rest' + Credential = $Credential + NcRestCredential = $NcRestCredential + NetworkControllerOid = $true + ErrorAction = 'Stop' + } + + $CertRotateConfig = New-SdnCertificateRotationConfig @certRotateConfigParams + } + + "Network Controller Rest Certificate {0} will be updated:`n`tCurrent: [Thumbprint:{1}]`n`tUpdated: [Thumbprint:{2}]" ` + -f $currentRestCert.Subject, $currentRestCert.Thumbprint, $CertRotateConfig.RestCertificate.Thumbprint | Trace-Output -Level:Warning + + # if we are not forcing the rotation, we will want to prompt the user to confirm the operation + if (!$Force) { + $confirm = Confirm-UserInput + if (-NOT $confirm) { + "User has opted to abort the operation. Terminating operation" | Trace-Output -Level:Warning + return + } + } + + # in situations where the rest certificate may be already expired, or the network controller is unhealthy + # we will want to leverage the expired certificate rotation function to fix up things so we may proceed with certificate rotate + if (!$isNetworkControllerHealthy) { + Start-SdnExpiredCertificateRotation @{ + CertRotateConfig = $CertRotateConfig + Credential = $Credential + NcRestCredential = $NcRestCredential + } + } + + # Rotate NC Northbound Certificate (REST) + "== STAGE: ROTATE NC REST CERTIFICATE ==" | Trace-Output + $null = Invoke-CertRotateCommand @{ + Command = 'Set-NetworkController' + Credential = $Credential + Thumbprint = $CertRotateConfig.RestCertificate.Thumbprint + } + + "Waiting for 5 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output + Start-Sleep -Seconds 300 + + # Rotate Cluster Certificate + "== STAGE: ROTATE NC CLUSTER CERTIFICATE ==" | Trace-Output + $null = Invoke-CertRotateCommand @{ + Command = 'Set-NetworkControllerCluster' + Credential = $Credential + Thumbprint = $CertRotateConfig.RestCertificate.Thumbprint + } + + "Waiting for 5 minutes before proceeding to the next step. Script will resume at {0}" -f (Get-Date).AddMinutes(5).ToUniversalTime().ToString() | Trace-Output + Start-Sleep -Seconds 300 + + ##################################### + # + # Certificate Seeding (Southbound Nodes) + # + ##################################### + + # in situation where maybe the Network Controller was down and we could not previously determine the southbound devices (if they exist) + # we will want to get updated infrastructure information to determine if we need to seed the certificate to the southbound devices + # if we get an error with returning the infrastructure information, we will terminate the script + if ($CertRotateConfig.RestCertificate.IsSelfSigned) { + $sdnFabricDetails = Get-SdnInfrastructureInfo -Credential $Credential -NcRestCredential $NcRestCredential -Force -ErrorAction Stop + $southBoundNodes = @() + if ($null -ne $sdnFabricDetails.LoadBalancerMux) { + $southBoundNodes += $sdnFabricDetails.LoadBalancerMux + } + if ($null -ne $sdnFabricDetails.Server) { + $southBoundNodes += $sdnFabricDetails.Server + } + + if ($southBoundNodes) { + "== STAGE: REST SELF-SIGNED CERTIFICATE SEEDING (Southbound Nodes) ==" | Trace-Output + + # ensure that we have the latest version of sdnDiagnostics module on the southbound devices + Install-SdnDiagnostics -ComputerName $southBoundNodes -Credential $Credential -ErrorAction Stop + + Copy-CertificateToFabric @{ + CertFile = $importRestCert.SelfSignedCertFileInfo.FullName + FabricDetails = $sdnFabricDetails + NetworkControllerRestCertificate = $true + InstallToSouthboundDevices = $true + Credential = $Credential + } + } + } + + "== STAGE: UPDATE X509 CREDENTIALS ==" | Trace-Output + $null = Update-NetworkControllerCredentialResource @{ + NcUri = "https://$($NcInfraInfo.NcRestName)" + Credential = $NcRestCredential + NewRestCertThumbprint = $CertRotateConfig.RestCertificate.Thumbprint + ErrorAction = 'Stop' + } + + ##################################### + # + # Restart services + # + ##################################### + + "== STAGE: RESTART NETWORK CONTROLLER SERVICES ==" | Trace-Output + # restart the network controller services + # this will force new TLS connections to be established to southbound devices + # ensuring that the new certificates are used and we are able to push policies successfully + + # check to determine if we have a multi-node NC cluster and if so, leverage the SF cmdlets to move the replicas + # otherwise, we will just stop the processes and let SF restart them automatically + if ($sdnFabricDetails.NetworkController.Count -gt 1) { + Move-SdnServiceFabricReplica -ServiceTypeName 'SlbManagerService' + Move-SdnServiceFabricReplica -ServiceTypeName 'VSwitchService' + } + else { + Get-Process -Name 'SDNFW' | Stop-Process -Force -ErrorAction Continue + Get-Process -Name 'SDNSLBM' | Stop-Process -Force -ErrorAction Continue + } + + "Certificate rotation has completed" | Trace-Output + } + catch { + + } +} diff --git a/src/modules/SdnDiag.NetworkController/public/Test-SdnCertificateRotationConfig.ps1 b/src/modules/SdnDiag.NetworkController/public/Test-SdnCertificateRotationConfig.ps1 index 04afebe6..e74dab68 100644 --- a/src/modules/SdnDiag.NetworkController/public/Test-SdnCertificateRotationConfig.ps1 +++ b/src/modules/SdnDiag.NetworkController/public/Test-SdnCertificateRotationConfig.ps1 @@ -15,7 +15,7 @@ function Test-SdnCertificateRotationConfig { [PSCustomObject[]]$NcNodeList, [Parameter(Mandatory = $true)] - [hashtable]$CertRotateConfig, + [CertRotateConfig]$CertRotateConfig, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] @@ -23,66 +23,52 @@ function Test-SdnCertificateRotationConfig { $Credential = [System.Management.Automation.PSCredential]::Empty ) - try { + $sb1 = { + param([Parameter(Position = 0)][String]$param1, [Parameter(Position = 1)][String]$param2) + $nodeCertObj = Get-SdnCertificate -Path $param1 -Thumbprint $param2 - if ([string]::IsNullOrEmpty($CertRotateConfig["NcRestCert"])) { - Trace-Output -Message "NcRestCert not specified in CertRotateConfig" -Level:Error + # ensure that certificate is present + if ($null -eq $nodeCertObj) { return $false } - $ncRestCert = $CertRotateConfig["NcRestCert"] - foreach ($ncNode in $NcNodeList) { - if ($CertRotateConfig["ClusterCredentialType"] -ieq "X509") { - $nodeCert = $CertRotateConfig[$ncNode.NodeName.ToLower()] - if ([string]::IsNullOrEmpty($nodeCert)) { - Trace-Output -Message "The ClusterCredentialType is X509 but Node $($ncNode.NodeName) does not have certificate specified" -Level:Error - return $false - } - else { - $certValid = Invoke-PSRemoteCommand -ComputerName $ncNode.IpAddressOrFQDN -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - $nodeCertObj = Get-SdnCertificate -Path "Cert:\LocalMachine\My" -Thumbprint $param1 - if ($null -eq $nodeCertObj) { - return $false - } - else { - if ($nodeCertObj.NotAfter -le (Get-Date)) { - return $false - } - } - return $true - } -ArgumentList $nodeCert + # ensure that certificate is not expired + if ($nodeCertObj.NotAfter -le (Get-Date)) { + return $false + } - if (!$certValid) { - Trace-Output -Message "Node $($ncNode.NodeName) does not have validate Node certificate with thumbprint $nodeCert installed" -Level:Error - return $false - } - } - } + return $true + } + + # validate that the certificates are installed on the nodes + $CertRotateConfig.NodeCerts | ForEach-Object { + if (Test-ComputerNameIsLocal -ComputerName $_.IpAddressOrFQDN) { + $certValid = Invoke-Command -ScriptBlock $sb1 -ArgumentList @("Cert:\LocalMachine\My", $_.Thumbprint) + } + else { + $certValid = Invoke-PSRemoteCommand -ComputerName $_.IpAddressOrFQDN -Credential $Credential -ScriptBlock $sb1 -ArgumentList @("Cert:\LocalMachine\My", $_.Thumbprint) + } - $certValid = Invoke-PSRemoteCommand -ComputerName $ncNode.IpAddressOrFQDN -Credential $Credential -ScriptBlock { - param([Parameter(Position = 0)][String]$param1) - $ncRestCertObj = Get-SdnCertificate -Path "Cert:\LocalMachine\My" -Thumbprint $param1 - if ($null -eq $ncRestCertObj) { - return $false - } - else { - if ($ncRestCertObj.NotAfter -le (Get-Date)) { - return $false - } - } - return $true - } -ArgumentList $ncRestCert + if (!$certValid) { + throw "$($_.NodeName) does not have valid certificate with thumbprint $($_.Thumbprint) installed" + } + } + + # validate the rest certificate exists on each of the nodes + if ($CertRotateConfig.RestCertificate) { + $NcNodeList | ForEach-Object { + if (Test-ComputerNameIsLocal -ComputerName $_.IpAddressOrFQDN) { + $certValid = Invoke-Command -ScriptBlock $sb1 -ArgumentList @("Cert:\LocalMachine\My", $CertRotateConfig.RestCertificate.Thumbprint) + } + else { + $certValid = Invoke-PSRemoteCommand -ComputerName $_.IpAddressOrFQDN -Credential $Credential -ScriptBlock $sb1 -ArgumentList @("Cert:\LocalMachine\My", $CertRotateConfig.RestCertificate.Thumbprint) + } if (!$certValid) { - Trace-Output -Message "Node $($ncNode.NodeName) does not have validate NcRest certificate with thumbprint $ncRestCert installed" -Level:Error - return $false + throw "$($_.NodeName) does not have valid rest certificate with thumbprint $($CertRotateConfig.RestCertificate.Thumbprint) installed" } } - return $true - } - catch { - $_ | Trace-Exception - $_ | Write-Error } + + return $true } diff --git a/src/modules/SdnDiag.Server/public/Get-SdnServerCertificate.ps1 b/src/modules/SdnDiag.Server/public/Get-SdnServerCertificate.ps1 index 66e8f89d..0f0ec2e7 100644 --- a/src/modules/SdnDiag.Server/public/Get-SdnServerCertificate.ps1 +++ b/src/modules/SdnDiag.Server/public/Get-SdnServerCertificate.ps1 @@ -1,16 +1,31 @@ function Get-SdnServerCertificate { <# .SYNOPSIS - Returns the certificate used by the SDN Host Agent. + Returns the certificate used by the Network Controller Host Agent. + .PARAMETER NetworkControllerOid + Specifies to return only the certificate that has the specified Network Controller OID. #> [CmdletBinding()] - param() + param ( + [Parameter(Mandatory = $false)] + [switch]$NetworkControllerOid + ) try { $serverCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' -Name 'HostAgentCertificateCName' $subjectName = "CN={0}" -f $serverCert - $certificate = Get-SdnCertificate -Subject $subjectName -Path 'Cert:\LocalMachine\My' + $certificate = Get-SdnCertificate -Subject $subjectName -Path 'Cert:\LocalMachine\My' -NetworkControllerOid:$NetworkControllerOid + + if ($null -eq $certificate) { + if ($NetworkControllerOid) { + throw New-Object System.NullReferenceException("Failed to locate certificate for NCHostAgent containing Network Controller OID") + } + else { + throw New-Object System.NullReferenceException("Failed to locate certificate for NCHostAgent") + } + } + return $certificate } catch { diff --git a/src/modules/SdnDiag.Server/public/New-SdnServerCertificate.ps1 b/src/modules/SdnDiag.Server/public/New-SdnServerCertificate.ps1 index bc6d7dc6..f4c53dda 100644 --- a/src/modules/SdnDiag.Server/public/New-SdnServerCertificate.ps1 +++ b/src/modules/SdnDiag.Server/public/New-SdnServerCertificate.ps1 @@ -37,27 +37,24 @@ function New-SdnServerCertificate { } # ensure that the module is running as local administrator - $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-NOT $elevated) { - throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.") - } + Confirm-IsAdmin try { if (-NOT (Test-Path -Path $Path -PathType Container)) { "Creating directory {0}" -f $Path | Trace-Output - $CertPath = New-Item -Path $Path -ItemType Directory -Force + $certPath = New-Item -Path $Path -ItemType Directory -Force } else { - $CertPath = Get-Item -Path $Path + $certPath = Get-Item -Path $Path } - $serverCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' -Name 'HostAgentCertificateCName' + $serverCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' -Name 'HostAgentCertificateCName' -ErrorAction Stop $subjectName = "CN={0}" -f $serverCert - $certificate = New-SdnCertificate -Subject $subjectName -NotAfter $NotAfter + $certificate = New-SdnSelfSignedCertificate -Subject $subjectName -NotAfter $NotAfter # after the certificate has been generated, we want to export the certificate and save the file to directory # This allows the rest of the function to pick up these files and perform the steps as normal - [System.String]$cerFilePath = "$(Join-Path -Path $CertPath.FullName -ChildPath $subjectName.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).cer" + [System.String]$cerFilePath = "$(Join-Path -Path $certPath.FullName -ChildPath $subjectName.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).cer" "Exporting certificate to {0}" -f $cerFilePath | Trace-Output $exportedCertificate = Export-Certificate -Cert $certificate -FilePath $cerFilePath -Type CERT Copy-CertificateToFabric -CertFile $exportedCertificate.FullName -FabricDetails $FabricDetails -ServerNodeCert -Credential $Credential @@ -73,4 +70,6 @@ function New-SdnServerCertificate { $_ | Trace-Exception $_ | Write-Error } + + return $null } diff --git a/src/modules/SdnDiag.Server/public/Start-SdnServerCertificateRotation.ps1 b/src/modules/SdnDiag.Server/public/Start-SdnServerCertificateRotation.ps1 index 12ea4f59..bbf1ac87 100644 --- a/src/modules/SdnDiag.Server/public/Start-SdnServerCertificateRotation.ps1 +++ b/src/modules/SdnDiag.Server/public/Start-SdnServerCertificateRotation.ps1 @@ -8,83 +8,49 @@ function Start-SdnServerCertificateRotation { Specifies a user account that has permission to access the northbound NC API interface. The default is the current user. .PARAMETER GenerateCertificate Switch to determine if certificate rotate function should generate self-signed certificates. - .PARAMETER CertPath - Path directory where certificate(s) .pfx files are located for use with certificate rotation. .PARAMETER CertPassword SecureString password for accessing the .pfx files, or if using -GenerateCertificate, what the .pfx files will be encrypted with. .PARAMETER NotAfter Expiration date when using -GenerateCertificate. If ommited, defaults to 3 years. - .PARAMETER CertRotateConfig - The Config generated by New-SdnCertificateRotationConfig to include appropriate certificate thumbprints for server nodes. .PARAMETER Force Switch to force the rotation without being prompted, when Service Fabric is unhealthy. #> [CmdletBinding(DefaultParameterSetName = 'GenerateCertificate')] param ( - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] [System.String]$NetworkController, - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, - [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] - [System.String]$CertPath, - [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] [Switch]$GenerateCertificate, - [Parameter(Mandatory = $true, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $true, ParameterSetName = 'GenerateCertificate')] [System.Security.SecureString]$CertPassword, [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] [datetime]$NotAfter = (Get-Date).AddYears(3), - [Parameter(Mandatory = $true, ParameterSetName = 'CertConfig')] - [hashtable]$CertRotateConfig, - - [Parameter(Mandatory = $false, ParameterSetName = 'Pfx')] [Parameter(Mandatory = $false, ParameterSetName = 'GenerateCertificate')] - [Parameter(Mandatory = $false, ParameterSetName = 'CertConfig')] [switch]$Force ) - # these are not yet supported and will take a bit more time to implement as it touches on core framework for rotate functionality - # however majority of the environments impacted are using sdnexpress which leverage self-signed certificates. - if ($CertRotateConfig -or $CertPath) { - "This feature is not yet supported and is under development. Please use -GenerateCertificate or reference {0} for manual steps." ` - -f 'https://learn.microsoft.com/en-us/azure-stack/hci/manage/update-network-controller-certificates?tabs=manual-renewal' | Trace-Output -Level:Warning - return - } - # ensure that the module is running as local administrator - $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-NOT $elevated) { - throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.") - } + Confirm-IsAdmin - # add disclaimer that this feature is currently under preview - if (!$Force) { - "This feature is currently under preview. Please report any issues to https://github.com/microsoft/SdnDiagnostics/issues so we can accurately track any issues and help unblock your cert rotation." | Trace-Output -Level:Warning - $confirm = Confirm-UserInput -Message "Do you want to proceed with certificate rotation? [Y/N]:" - if (-NOT $confirm) { - "User has opted to abort the operation. Terminating operation" | Trace-Output -Level:Warning - return - } + $config = Get-SdnModuleConfiguration -Role 'NetworkController' + $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.windowsFeature + if (-NOT ($confirmFeatures)) { + throw New-Object System.NotSupportedException("The current machine is not a NetworkController, run this on NetworkController.") } $array = @() diff --git a/src/modules/SdnDiag.Utilities/private/Confirm-IsAdmin.ps1 b/src/modules/SdnDiag.Utilities/private/Confirm-IsAdmin.ps1 new file mode 100644 index 00000000..c33bb8f7 --- /dev/null +++ b/src/modules/SdnDiag.Utilities/private/Confirm-IsAdmin.ps1 @@ -0,0 +1,11 @@ +function Confirm-IsAdmin { + <# + .SYNOPSIS + Confirms that the current user is an administrator. + #> + + $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if (-NOT $elevated) { + throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.") + } +}