I've created a TCP service that creates a secure SSL connection that I am trying to host in an Azure Service Fabric cluster. While there is documentation on how to load and use SSL certificates for sites and API's I can't seem to find any documentation on how I would load my cert for my service. I have loaded my cert to a key vault but now need to create an instance of X509Certificate2 to secure my tcp connection.
4 Answers
One option would be to run a Service Fabric setup task as SYSTEM
(only SYSTEM
will have permissions to change ACL on the certs) and give NETWORK SERVICE
permissions to read your cert
Full example below, note I set ACLs on all LocalMachine / My certs, modify the PS script to only get certs you need
ServiceManifest.xml
<CodePackage Name="Code" Version="1.0.0">
<SetupEntryPoint>
<ExeHost>
<Program>setup.cmd</Program>
<WorkingFolder>CodePackage</WorkingFolder>
</ExeHost>
</SetupEntryPoint>
<EntryPoint>
<ExeHost>
<Program>XXX.exe</Program>
<WorkingFolder>CodePackage</WorkingFolder>
</ExeHost>
</EntryPoint>
</CodePackage>
setup.cmd
powershell.exe -NoLogo -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File configure-certs.ps1
configure-certs.ps1
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store @(
[System.Security.Cryptography.X509Certificates.StoreName]::My,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
try
{
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$certs = $store.Certificates
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule @(
"NETWORK SERVICE",
[System.Security.AccessControl.FileSystemRights]::Read,
[System.Security.AccessControl.AccessControlType]::Allow)
foreach ($cert in $certs)
{
$privateKeyPath = "$($ENV:ProgramData)\Microsoft\Crypto\RSA\MachineKeys\$($cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName)"
$privateKeyInfo = New-Object System.IO.FileInfo @($privateKeyPath)
$privateKeyAccessControl = $privateKeyInfo.GetAccessControl()
$privateKeyAccessControl.AddAccessRule($rule)
$privateKeyInfo.SetAccessControl($privateKeyAccessControl)
}
}
finally
{
$store.Close()
}
ApplicationManifest.xml
After ConfigOverrides as a child of ServiceManifestImport of the package you're adding setup to
...
<Policies>
<RunAsPolicy CodePackageRef="Code" UserRef="LocalSystem" EntryPointType="Setup" />
</Policies>
...
After DefaultServices as a child of ApplicationManifest
...
<Principals>
<Users>
<User Name="LocalSystem" AccountType="LocalSystem" />
</Users>
</Principals>
...
We had a similar problem (we wanted to use Kestrel with HTTPS) and found two ways of achieving this.
- Dump the certificate into the VM's keystore using an ARM script & Keyvault then read it out using an X509Store.
There were issues with this approach as AFAIK service fabric runs under NETWORK_SERVICE and did not have permissions to read private keys.
- Our fallback was reading the certificate & password as secrets from the key vault then creating a new X509Certificate2. We did this by giving an azure AD service principal access to read the secrets then using a KeyVaultClient to obtain them.
This is better explained here - https://azure.microsoft.com/en-gb/documentation/articles/key-vault-use-from-web-application
I would love it if anyone could advise what is best practice in this scenario - I think the second option has issues in that creating a new X509Certificate2 also creates the cert in a temp location on disk which is not cleaned up, but i'm sure there are more.
When I originally created my cluster it was an unsecured cluster. Simply by recreating it as a secured cluster I can now access the certificates in the vault used to secure the clusters using the the following helper method I created.
private static X509Certificate GetServerCertificate(string thumbprint)
{
string thumbprint = CertificateThumbprint;
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificateCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
if (certificateCollection.Count == 0)
{
store.Close();
string errorMessage = string.Format("Unable to load certificate with thumbprint {0}", thumbprint);
throw new ApplicationException(errorMessage);
}
else
{
var certificate = new X509Certificate2(certificateCollection[0]);
store.Close();
return certificate;
}
}
Instructions on creating a secure cluster can be found here: https://azure.microsoft.com/en-us/documentation/articles/service-fabric-cluster-creation-via-arm/
There's actually a much simpler way of making sure that Service Fabric (e.g. default Network_Service) has permission to access your certificate.
I run an on-premise Service Fabric cluster, and I had to do a similar thing in the past as I use certificates to encrypt application secrets.
Basically, all you have to do is update your Application Manifest to
- Define the network account (used to access the certificate) in
<Principals>
- Add
<SecurityAccessPolicy>
that associates the network account with the certificate - Identify the Certificate in
<Certificates>
Example ApplicationManifest.xml
extract:
<Principals>
<Users>
<User Name="DefaultServiceAccount" AccountType="NetworkService" />
</Users>
</Principals>
<Policies>
<SecurityAccessPolicies>
<SecurityAccessPolicy ResourceRef="AppSecretsCert" PrincipalRef="DefaultServiceAccount" ResourceType="Certificate" />
</SecurityAccessPolicies>
</Policies>
<Certificates>
<SecretsCertificate X509FindValue="YOUR CERT THUMPRINT" Name="AppSecretsCert" />
</Certificates>