Sunday, 15 April 2012

Azure Web roles, configuring IIS - basic auth / ssl example

Windows Azure roles have the minimum configuration which covers most needs, but when you need to ensure that your deployment works with additional features some configuration is required. The best place to do this is when the web role starts. In this example I want to ensure that basic authentication is possible with my azure hosted website against a local NT account; so the basic authentication role module needs to be added to IIS7 when the role starts, I also need to unlock the config sections so that my web.config can configure the authentication (disable anonymous, enable basic authentication and allow SSL). Step 1. Create the file in the startup folder, first off allow powershell: enablepowershell.cmd - containing a single line:
  
powershell -command "set-executionpolicy Unrestricted"&

Step 2. A command file to add basic-auth and "unlock" the IIS config sections (easy one to forget!) that I require here:

configiis.cmd
  
ServerManagerCmd.exe -install web-basic-auth 

%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/access 

%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/authentication/anonymousAuthentication 

%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/authentication/basicAuthentication 


The basic principle can be extended to cover other scenarios. Step 3. Finally, some powershell to add a user "AddUser.ps1", in this example I'll configure the basic authentication such that a local user account provides the credentials, but as such an account doesn't yet exist, I will need to create it :

  $username="test"
  $computer = [ADSI]"WinNT://localhost"
  $user_obj = $computer.Create("user", "$username")
  $user_obj.SetPassword("testpassword1!")
  $user_obj.SetInfo()
  Write-Host "$username created."


Plenty of examples out there about how to make this a bit tighter. Now for tying this in with my azure web role. First off I want to up the OS version in my azure config (.cscfg) to version 2 (2008R2 I believe)

<ServiceConfiguration serviceName="AzureMocks" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="2" osVersion="*" >
.
.
</ServiceConfiguration >


Next I need to add some sections to my service definition file (.csdef) This runs the command files under elevated privs.

    <Startup>
      <Task commandLine="Startup\enablepowershell.cmd" executionContext="elevated" />
      <Task commandLine="Startup\configiis.cmd" executionContext="elevated" />
    </Startup >
    <Runtime executionContext="elevated" />

Lastly in my web role start I call the powershell "AddUser.ps1"

            
            try
            {
                Startup.RunPowershellConfig(@".\startup\AddUser.ps1", "AddUser.ps1.txt");

            }
            catch (Exception e)
            {
                RoleDiagnosticsHelper.WriteExceptionToBlobStorage(e, @"An error occured running the powershell startup script '.\startup\AddUser.ps1'");
                return false;
            }
Where those functions are defined as
  

        public static void RunPowershellConfig(string path, string outPath)
        {
            var startInfo = new ProcessStartInfo
            {
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden,
                FileName = "powershell.exe",
                Arguments = path,
                RedirectStandardOutput = true,
                UseShellExecute = false,
            };

            var writer = new System.IO.StreamWriter(outPath);
            var process = Process.Start(startInfo);

            process.WaitForExit();

            writer.Write(process.StandardOutput.ReadToEnd());
            writer.Close();
        }

        public static void WriteExceptionToBlobStorage(Exception ex, string additionalInfo)
        {
            if (null == additionalInfo)
                additionalInfo = string.Empty;

            var storageAccount = CloudStorageAccount.Parse(
                RoleEnviroment.GetConfigurationSettingValue(
                  "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString"));

            var container = storageAccount.CreateCloudBlobClient()
              .GetContainerReference("rolestartexceptions");
            container.CreateIfNotExist();

            var blob = container.GetBlobReference(string.Format(
              "role-start-exception-{0}-{1}.log",
               RoleEnvironment.CurrentRoleInstance.Id,
               DateTime.UtcNow.ToLongDateString()));
            
            // tostring should include inner exception if exists
            blob.UploadText(ex + " Additional information " + additionalInfo);
        }


I am careful to ensure that I catch and write any exceptions, a problem on role start can be tricky to find and fix otherwise. See cweyers post here
Of course I should not forget, my website config needs the required settings to actually allow SSL and basic auth now all the above are complete


  <system.webServer>
    <security xdt:Transform="Replace">
      <access sslFlags="Ssl" />
      <authentication>
        <anonymousAuthentication enabled="false"/>
        <basicAuthentication enabled="true"/>
      </authentication>
    </security>
  </system.webServer>


Important note
When running all this locally in the emulator that last bit of config can cause some problems, if you spin the site or service up in the emulator the debugger may refuse to attach (we've reported this as a bug under tools version 1.6 Nov 2011 and I believe it's known. The work around is to comment the offending section and when the emulator role starts, go into IIS and configure through the management console. Can use a config transform to ensure the config really does go in for real as this issue appears to be limited to the emulator. So if you see a weird emulator error complaining about invalid site, the config is a good place to start.

No comments: