0
votes

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

4 Answers

2
votes

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>
...
1
votes

We had a similar problem (we wanted to use Kestrel with HTTPS) and found two ways of achieving this.

  1. 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.

  1. 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.

1
votes

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/

1
votes

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

  1. Define the network account (used to access the certificate) in <Principals>
  2. Add <SecurityAccessPolicy> that associates the network account with the certificate
  3. 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>