I have been struggling to get secure WCF services running on Azure, not because it doesn’t work, just because I am not up to speed on the combinations and sometimes fail to think it through before I start.
Anyways, what I am trying to do in this post is instead of describing the end solution of how to make it work I am taking you through the common errors you may get when you are securing your WCF service using HTTPS and Certificates on Azure. As always with security, don’t take anyone’s word for it how things should be configured but try to understand the reasoning why. I am saying this because I am by no means a security expert and although I made things work for me and I am writing this blog, things can always be done better and tomorrow I probably find a better way of securing my service but alas this is today.
Requirements
I think, before you try to use some of my mentioned solutions, I recommend you look at how close your environment is to mine before you attempt to copy some of the settings. As you all know everything is currently evolving rapidly and the screens and settings you can do with VS2010 beta 2 may not be available in the RTM version and may not be the same in the version of the IDE you are currently using.
Ok, here goes. I am using Visual Studio 2010 Beta 2, on Windows 7 Ultimate, with the latest SDK’s as of Jan 29th 2010 and the Azure platform version at the time of this writing.
Another note, just to be sure, you need to use F5 to properly run and debug the Azure solution to get the proper results. If you just use “View in Browser” you will see your service but you may not see the changes you are making to your config file.. I am just saying.
Start Point / Article Reference
The article most valuable to get started with Azure and Certificates comes from Jim Nakashima which is called How to: Add an HTTPS Endpoint to a Windows Azure Cloud Service. I followed his article exactly to get the Certificate, whether self-signed or bought, in my Azure project and in the Azure portal. I hope that this article would be a good follow up on Jim’s article to configure the service and the (Windows) client.
I made a little sample solution to replay the start of a solution to be at the same point where Jim finished in his article. This would be,
- You have uploaded your certificate to Azure
- You have configured your WebRole to have TWO Input endpoints (HTTP and HTTPS) of which the HTTPS endpoint is configured with your certificate.
NOTE: I am mentioning the TWO endpoints as this is how Jim configured his web role and it leads into the first issue.
- You have a basic web.config which successfully launches your service in either http or https mode.
I am showing my base system.serviceModel node below. I cleaned the comments and changed the service and behaviour names for clarity but you should be able to recognize that it is pretty much an out-of-the-box configuration.
1: <system.serviceModel>
2: <services>
3: <service name="WCFServiceWebRole.Service"
4: behaviorConfiguration="ServiceBehavior">
5: <endpoint address=""
6: binding="basicHttpBinding"
7: contract="WCFServiceWebRole.IService1">
8: </endpoint>
9: <endpoint address="mex"
10: binding="mexHttpBinding"
11: contract="IMetadataExchange"/>
12: </service>
13: </services>
14: <behaviors>
15: <serviceBehaviors>
16: <behavior name="ServiceBehavior">
17: <serviceMetadata httpGetEnabled="true"/>
18: <serviceDebug includeExceptionDetailInFaults="false"/>
19: </behavior>
20: </serviceBehaviors>
21: </behaviors>
22: </system.serviceModel>
Connecting a client
To test the service and its configuration I always create a little test harness, just a little console app which allows me to connect to the service, build up the basic code and configuration. The less code you have to deal with the easier it is to troubleshoot the problems you encounter.
So if you create a console app and create a service reference to the service, which by default is focused on the HTTP protocol you probably get an app.config which looks similar as the one below.
1: <system.serviceModel>
2: <bindings>
3: <basicHttpBinding>
4: <binding name="BasicHttpBinding_IService1" closeTimeout="00:01:00"
5: openTimeout="00:01:00" receiveTimeout="00:10:00"
6: sendTimeout="00:01:00" allowCookies="false"
7: bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
8: maxBufferSize="65536" maxBufferPoolSize="524288"
9: maxReceivedMessageSize="65536" messageEncoding="Text"
10: textEncoding="utf-8" transferMode="Buffered"
11: useDefaultWebProxy="true">
12:
13: <readerQuotas maxDepth="32"
14: maxStringContentLength="8192"
15: maxArrayLength="16384"
16: maxBytesPerRead="4096"
17: maxNameTableCharCount="16384" />
18:
19: <security mode="None">
20: <transport clientCredentialType="None"
21: proxyCredentialType="None"
22: realm="" />
23: <message clientCredentialType="UserName"
24: algorithmSuite="Default" />
25: </security>
26: </binding>
27: </basicHttpBinding>
28: </bindings>
29: <client>
30: <endpoint address="http://localhost:1433/Service.svc"
31: binding="basicHttpBinding"
32: bindingConfiguration="BasicHttpBinding_IService1"
33: contract="ServiceReferenceProxy.IService1"
34: name="BasicHttpBinding_IService1" />
35: </client>
36: </system.serviceModel>
Test if your console app can connect to the service and execute a simple method. That is all we do for now with the console app.
One Service Endpoint only
The first thing I do is to eliminate that the HTTP endpoint is available. The way you do this is by selecting the “Properties” of your WebRole, select the ‘Endpoints’ tab as shown here and uncheck the ‘HTTP’ checkbox.
When you save the setting and hit ‘F5’ you probably are confronted with the following error:
Could not find a base address that matches scheme http for the endpoint with binding BasicHttpBinding. Registered base address schemes are [https].
What I did was I unchecked the HTTP Input endpoint on the Azure Web Role which leaves just the HTTPS Input endpoint left over. What it means is that you are accessing the service over the HTTPS transport protocol while you have not defined a binding which matches your intentions.
To remedy this message you need to instruct the binding that the security mode has now changed to “Transport”. You can change this setting by introducing a bindings section and configure the basicHttpBinding you are using currently.
Step 1. Below is the bindings snippet you can add to your base config mentioned above.
1: <bindings>
2: <basicHttpBinding>
3: <binding name="UnSecureBinding">
4: <security mode="Transport">
5: <transport clientCredentialType="None" proxyCredentialType="None"/>
6: </security>
7: </binding>
8: </basicHttpBinding>
9: </bindings>
Step 2. Add the bindingConfiguration attribute to the service endpoint and use the value “UnSecureBinding”.
Hit F5 and now when your service comes up you won’t see this error message anymore but now you get the following:
Could not find a base address that matches scheme http for the endpoint with binding MetadataExchangeHttpBinding. Registered base address schemes are [https].
Now you might thing that this is the exact same error we had before but notice the MetadataExchangeHttpBinding addition to the message.
This is easy to solve by changing the binding of the mex endpoint.
Step 1. Change the binding of the mex endpoint from mexHttpBinding to mexHttpsBinding, notice the appearance of the ‘S’.
Ok, cool, hit F5 again.. you probably think this guy is nuts, going from one error to the next… I don’t think anybody I know would disagree with you on that :-) however the message you probably see is the following:
The HttpGetEnabled property of ServiceMetadataBehavior is set to true and the HttpGetUrl property is a relative address, but there is no http base address. Either supply an http base address or set HttpGetUrl to an absolute address.
Now the answer seems to be clear, add a base address but that means we are stuck with a base address in our config which we then need to maintain plus didn’t we want to get rid of the HTTP endpoint?
Step 1. To solve this message change the httpGetEnabled attribute on the serviceMetadata element to httpsGetEnabled, again notice the appearance of the ‘S‘ in the attribute.
Ok, now our service should come up and only one Service Input endpoint should be accessible which would be the HTTPS one.
Bindings
Now if you think “basicHttpBinding and SSL how does that work?” I need to refer you to the documentation, there is plenty out there and you need to choose the binding that makes the most sense for you. By default all bindings with the exception of the BasicHttpBinding have security enabled but it doesn’t mean you cannot turn on security on the BasicHttpBinding as you have seen here.
There is a MSDN page which gives you a good overview of the bindings and its features Configuring System-Provided Bindings
The Client App Again
Now that we have the service configured to a point that it allows only secure connections via HTTPS, it is time that we look at our client app to see if it would connect to the service.
Now, it is good to understand that the WCF configuration for the client need to match what you expect at the Service. If you look at the basic configuration of the client app (shown earlier) you can see that the security mode is by default set to “None”. If you leave it like this you will get the same error as before (Could not find a base address that matches scheme http for the endpoint with binding BasicHttpBinding. Registered base address schemes are [https].)
To test our client app follow the two steps to get started.
Step 1. Change the “mode” attribute of the “security” element from “None” to “Transport”
Step 2. Change the “address” attribute of the “endpoint” element to match the Azure Service endpoint which, in my case, is https://127.0.0.1/Service.svc
Step 3. Add some code to your client app to execute a method on your service. I am using the template method which is created by default when you create a new WCFServiceWebRole called GetData.
1: using System;
2: using TestConsole.ServiceReferenceProxy;
3:
4: namespace TestConsole
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: Console.Write("Press Enter to start: ");
11: Console.ReadLine();
12:
13: using (Service1Client proxy = new Service1Client())
14: {
15: Console.WriteLine(proxy.GetData(23));
16: }
17:
18: Console.ReadLine();
19: }
20: }
21: }
When you hit “F5” and execute the console app it might give you the following error message:
Could not establish trust relationship for the SSL/TLS secure channel with authority '127.0.0.1'.
This is a typical problem on a developer faces as you would most likely work with a test (self-signed) certificate. In the Azure case, that appFabric uses a “special” developers certificate. This certificate is not root trusted and therefore the authentication with the service fails.
If you followed Jim’s blog posting you noticed that he suggested to move this developer certificate from the “Personal\Certificates” folder to the “Trusted Root Certification Authorities” Certificate store. While this works well and you can continue development right away you must be aware of the risk. I am not going to explain all this as the intention of this post is about Certificate configuration.
I followed Jim’s option and moved my developer cert in to the aforementioned store. Now my client app runs and I get the results back from the Service.
If I hit “F5” my client app is able to connect to the service and it returns the string from the method call, so all is working well.
Azure Bug??
This would be a good time to publish your Azure package to you Azure account. If you had uploaded your solution to Azure before you started reading this article and followed the changes I made in regards to the Single Endpoint only configuration, if you now upload your package you probably get the following warning from Azure:
Different number of input endpoints for role…
The weird thing is that I named my deployment package Sample-2 which is clearly visible above the cube. It looks like the upgrade was successful but when I tried to Suspend and Run the instance I didn’t have the feeling that it had deployed my latest settings, which I confirmed by just hitting the service over http and it still came up.
As far as I know then only way to get this problem solved is to delete the instance and deploy your latest package.
Back to the Client App… Again
Now that I have my package running on Azure, I am going to test my client app and see if I can hit the service which now runs on Azure. I change the address of the client endpoint to match the url on Azure. Start the client console app and…
Could not establish trust relationship for the SSL/TLS secure channel with authority '5dc2aad1aedc4fc1a54ae2b1ef199fa5.cloudapp.net'.
Hey, isn’t that what we solved just a minute ago? We placed our development cert in the “Trusted Root Certification Authorities” store so how come we get this message all of a sudden. Well, you are now running your service on Azure and Azure is now using you self-signed cert which you specified in your development solution. You might think, Oh I can solve this, I just do the same thing with my self-signed cert and drag it to the “Trusted Root Certification Authorities” store but as you probably found out quickly after, it doesn’t solve the problem.
There are two problems with self-signed or test certificates:
- The Certificate will fail the default certificate validation since the client uses ChainTrust by default. You could solve this by moving the test cert to the Trusted People store and use the “PeerTrust” certificateValidationMode,
- The second problem is that WCF by default expects the service certificate name to match the service endpoint (domain name). Typically you could solve this by adding the “identity” and “dns” elements to the endpoint.
However none of the suggestions have worked for me using Azure. The only thing which does work but should only be used for the purpose of test certificates is the following code snippet which introduces the ServicePointManager.ServerCertificateValidationCallback property.
1: using System;
2: using TestConsole.ServiceReferenceProxy;
3: using System.Net;
4: using System.Security.Cryptography.X509Certificates;
5: using System.Net.Security;
6:
7: namespace TestConsole
8: {
9: class Program
10: {
11: static void Main(string[] args)
12: {
13: ServicePointManager.ServerCertificateValidationCallback += RemoteCertificateValidate;
14:
15: Console.Write("Press Enter to start: ");
16: Console.ReadLine();
17:
18: using (Service1Client proxy = new Service1Client())
19: {
20: Console.WriteLine(proxy.GetData(23));
21: }
22:
23: Console.ReadLine();
24: }
25:
26: private static Boolean RemoteCertificateValidate(
27: Object sender, X509Certificate cert,
28: X509Chain chain, SslPolicyErrors error)
29: {
30: return true;
31: }
32: }
33: }
So far my post about using Certificates, I’ll let you know once I have a better solution or when I have a production certificate what the configuration should be. In the mean time let me know if you have any suggestions or comments.