Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release23dev #92

Merged
merged 10 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.3.0
* Added Sql Server Binding Support

2.2.2
* Removed empty constructor to resolve PAM provider error when using WinCert store types

Expand Down
2 changes: 1 addition & 1 deletion IISU/ClientPSCertStoreManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st
{
_logger.LogTrace("ps Has Errors");
var psError = ps.Streams.Error.ReadAll()
.Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message);
.Aggregate(string.Empty, (current, error) => current + error?.ErrorDetails.Message);
{
return new JobResult
{
Expand Down
373 changes: 373 additions & 0 deletions IISU/ClientPsSqlManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
// Copyright 2022 Keyfactor
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Keyfactor.Logging;
using Keyfactor.Orchestrators.Common.Enums;
using Keyfactor.Orchestrators.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Management.Infrastructure.Serialization;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Security.Cryptography.X509Certificates;

namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore
{
internal class ClientPsSqlManager
{
private string SqlServiceUser { get; set; }
private string SqlInstanceName { get; set; }
private bool RestartService { get; set; }
private string RegistryPath { get; set; }
private string RenewalThumbprint { get; set; } = "";
private string ClientMachineName { get; set; }
private long JobHistoryID { get; set; }
private readonly ILogger _logger;
private readonly Runspace _runSpace;

private PowerShell ps;

public ClientPsSqlManager(ManagementJobConfiguration config, string serverUsername, string serverPassword)
{
_logger = LogHandler.GetClassLogger<ClientPsSqlManager>();

try
{
ClientMachineName = config.CertificateStoreDetails.ClientMachine;
JobHistoryID = config.JobHistoryId;

if (config.JobProperties.ContainsKey("InstanceName"))
{
var instanceRef = config.JobProperties["InstanceName"]?.ToString();
SqlInstanceName = string.IsNullOrEmpty(instanceRef) ? "MSSQLSERVER":instanceRef;
}

// Establish PowerShell Runspace
var jobProperties = JsonConvert.DeserializeObject<JobProperties>(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate });
string winRmProtocol = jobProperties.WinRmProtocol;
string winRmPort = jobProperties.WinRmPort;
bool includePortInSPN = jobProperties.SpnPortFlag;
RestartService = jobProperties.RestartService;

_logger.LogTrace($"Establishing runspace on client machine: {ClientMachineName}");
_runSpace = PsHelper.GetClientPsRunspace(winRmProtocol, ClientMachineName, winRmPort, includePortInSPN, serverUsername, serverPassword);
}
catch (Exception e)
{
throw new Exception($"Error when initiating a SQL Management Job: {e.Message}", e.InnerException);
}
}

public ClientPsSqlManager(InventoryJobConfiguration config,Runspace runSpace)
{
_logger = LogHandler.GetClassLogger<ClientPsSqlManager>();

try
{
ClientMachineName = config.CertificateStoreDetails.ClientMachine;
JobHistoryID = config.JobHistoryId;

// Establish PowerShell Runspace
var jobProperties = JsonConvert.DeserializeObject<JobProperties>(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate });
string winRmProtocol = jobProperties.WinRmProtocol;
string winRmPort = jobProperties.WinRmPort;
bool includePortInSPN = jobProperties.SpnPortFlag;

_logger.LogTrace($"Establishing runspace on client machine: {ClientMachineName}");
_runSpace = runSpace;
}
catch (Exception e)
{
throw new Exception($"Error when initiating a SQL Management Job: {e.Message}", e.InnerException);
}
}

public JobResult UnBindCertificate()
{
try
{
_logger.MethodEntry();

_runSpace.Open();
ps = PowerShell.Create();
ps.Runspace = _runSpace;

RegistryPath = GetSqlCertRegistryLocation(SqlInstanceName, ps);

var funcScript = string.Format($"Clear-ItemProperty -Path \"{RegistryPath}\" -Name Certificate");
foreach (var cmd in ps.Commands.Commands)
{
_logger.LogTrace("Logging PowerShell Command");
_logger.LogTrace(cmd.CommandText);
}

_logger.LogTrace($"funcScript {funcScript}");
ps.AddScript(funcScript);
_logger.LogTrace("funcScript added...");
ps.Invoke();
_logger.LogTrace("funcScript Invoked...");

if (ps.HadErrors)
{
var psError = ps.Streams.Error.ReadAll()
.Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message);
{
return new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
JobHistoryId = JobHistoryID,
FailureMessage = $"Unable to unbind certificate to Sql Server"
};
}
}

return new JobResult
{
Result = OrchestratorJobStatusJobResult.Success,
JobHistoryId = JobHistoryID,
FailureMessage = ""
};
}
catch (Exception e)
{
return new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
JobHistoryId = JobHistoryID,
FailureMessage = $"Error Occurred in unbind {LogHandler.FlattenException(e)}"
};
}
finally
{
_runSpace.Close();
ps.Runspace.Close();
ps.Dispose();
}
}

public string GetSqlInstanceValue(string instanceName,PowerShell ps)
{
try
{
var funcScript = string.Format(@$"Get-ItemPropertyValue ""HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL"" -Name {instanceName}");
foreach (var cmd in ps.Commands.Commands)
{
_logger.LogTrace("Logging PowerShell Command");
_logger.LogTrace(cmd.CommandText);
}

_logger.LogTrace($"funcScript {funcScript}");
ps.AddScript(funcScript);
_logger.LogTrace("funcScript added...");
var SqlInstanceValue = ps.Invoke()[0].ToString();
_logger.LogTrace("funcScript Invoked...");
ps.Commands.Clear();

if (!ps.HadErrors)
{
return SqlInstanceValue;
}
return null;
}
catch (Exception e)
{
throw new Exception($"Error when initiating getting instance name from registry: {e.Message}", e.InnerException);
}
}

public string GetSqlCertRegistryLocation(string instanceName,PowerShell ps)
{
return $"HKLM:\\SOFTWARE\\Microsoft\\Microsoft SQL Server\\{GetSqlInstanceValue(instanceName,ps)}\\MSSQLServer\\SuperSocketNetLib\\";
}

public string GetSqlServerServiceName(string instanceValue)
{
if(string.IsNullOrEmpty(instanceValue))
return string.Empty;

//Default SQL Instance has this format
if (instanceValue.Split('.')[1] == "MSSQLSERVER")
return "MSSQLSERVER";

//Named Instance service has this format
return $"MSSQL`${instanceValue.Split('.')[1]}";
}

public JobResult BindCertificates(string renewalThumbprint, X509Certificate2 x509Cert)
{
try
{
var bindingError = string.Empty;
RenewalThumbprint = renewalThumbprint;

_runSpace.Open();
ps = PowerShell.Create();
ps.Runspace = _runSpace;
if (!string.IsNullOrEmpty(renewalThumbprint))
{
var funcScript = string.Format(@$"(Get-ItemProperty ""HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server"").InstalledInstances");
ps.AddScript(funcScript);
_logger.LogTrace("funcScript added...");
var instances = ps.Invoke();
ps.Commands.Clear();
foreach (var instance in instances)
{
var regLocation = GetSqlCertRegistryLocation(instance.ToString(), ps);

funcScript = string.Format(@$"Get-ItemPropertyValue ""{regLocation}"" -Name Certificate");
ps.AddScript(funcScript);
_logger.LogTrace("funcScript added...");
var thumbprint = ps.Invoke()[0].ToString();
ps.Commands.Clear();

if (RenewalThumbprint.Contains(thumbprint, StringComparison.CurrentCultureIgnoreCase))
{
bindingError=BindCertificate(x509Cert, ps);
}
}
}
else
{
bindingError=BindCertificate(x509Cert, ps);
}

if (bindingError.Length == 0)
{
return new JobResult
{
Result = OrchestratorJobStatusJobResult.Success,
JobHistoryId = JobHistoryID,
FailureMessage = ""
};
}
else
{
return new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
JobHistoryId = JobHistoryID,
FailureMessage = bindingError
};
}

}
catch (Exception e)
{
return new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
JobHistoryId = JobHistoryID,
FailureMessage = $"Error Occurred in BindCertificates {LogHandler.FlattenException(e)}"
};
}
finally
{
_runSpace.Close();
ps.Runspace.Close();
ps.Dispose();
}

}
public string BindCertificate(X509Certificate2 x509Cert,PowerShell ps)
{
try
{
_logger.MethodEntry();


//If they comma separated the instance entry param, they are trying to install to more than 1 instance
var instances = SqlInstanceName.Split(',');

foreach (var instanceName in instances)
{
RegistryPath = GetSqlCertRegistryLocation(instanceName, ps);

var thumbPrint = string.Empty;
if (x509Cert != null)
thumbPrint = x509Cert.Thumbprint.ToLower(); //sql server config mgr expects lower

var funcScript = string.Format($"Set-ItemProperty -Path \"{RegistryPath}\" -Name Certificate {thumbPrint}");
foreach (var cmd in ps.Commands.Commands)
{
_logger.LogTrace("Logging PowerShell Command");
_logger.LogTrace(cmd.CommandText);
}

_logger.LogTrace($"funcScript {funcScript}");
ps.AddScript(funcScript);
_logger.LogTrace("funcScript added...");
ps.Invoke();
_logger.LogTrace("funcScript Invoked...");

_logger.LogTrace("Setting up Acl Access for Manage Private Keys");
ps.Commands.Clear();

//Get the SqlServer Service User Name
var serviceName = GetSqlServerServiceName(GetSqlInstanceValue(instanceName, ps));
funcScript = @$"(Get-WmiObject Win32_Service -Filter ""Name='{serviceName}'"").StartName";
ps.AddScript(funcScript);
_logger.LogTrace("funcScript added...");
SqlServiceUser = ps.Invoke()[0].ToString();
_logger.LogTrace("funcScript Invoked...");
_logger.LogTrace("Got service login user for ACL Permissions");
ps.Commands.Clear();

funcScript = $@"$thumbprint = '{thumbPrint}'
$Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {{ $_.Thumbprint -eq $thumbprint }}
$privKey = $Cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
$keyPath = ""$($env:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\""
$privKeyPath = (Get-Item ""$keyPath\$privKey"")
$Acl = Get-Acl $privKeyPath
$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule(""{SqlServiceUser.Replace("$", "`$")}"", ""Read"", ""Allow"")
$Acl.SetAccessRule($Ar)
Set-Acl $privKeyPath.FullName $Acl";

ps.AddScript(funcScript);
ps.Invoke();
_logger.LogTrace("ACL FuncScript Invoked...");

//If user filled in a service name in the store then restart the SQL Server Services
if (RestartService)
{
_logger.LogTrace("Starting to Restart SQL Server Service...");
ps.Commands.Clear();
funcScript = $@"Restart-Service -Name ""{serviceName}"" -Force";

ps.AddScript(funcScript);
ps.Invoke();
_logger.LogTrace("Invoked Restart SQL Server Service....");
}

if (ps.HadErrors)
{
var psError = ps.Streams.Error.ReadAll()
.Aggregate(string.Empty, (current, error) => current + error?.Exception.Message);
{
return psError;
}
}
}
return "";
}
catch (Exception e)
{
return LogHandler.FlattenException(e);
}

}
}
}

Loading
Loading