Tuesday, September 17, 2013

System.Transactions.TransactionScope Timing out

When using the System.Transactions.TransactionScope there is a default machine setting timeout of 10 minutes.  If you have a long running transaction and it runs for over 10 minutes, it will timeout with the following exception message:

"The operation is not valid for the state of the transaction."

And an inner exception message of:

"Transaction Timeout"


There appear to be three ways to deal with this:

1.) Setting the system.transactions machinesettings maxtimeout in the machine.config on the server.  This is done by adding the following to the machine.config:

<system.transactions>
<machineSettings maxTimeout="00:20:00" />
    </system.transactions>

This is a machine wide setting and it may interfere with administrative policies.  It directly effects all applications running on the server.

2.) Setting allowExeDefinition="MachineToApplication" on the system.transaction section group in the machine.config.  The default is "MachineOnly".  See below for machine.config configuration:

        <sectionGroup name="system.transactions" type="System.Transactions.Configuration.TransactionsSectionGroup, System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null">
           <section name="defaultSettings" type="System.Transactions.Configuration.DefaultSettingsSection, System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null"/>
           <section name="machineSettings" type="System.Transactions.Configuration.MachineSettingsSection, System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null" allowDefinition="MachineOnly" allowExeDefinition="MachineToApplication"/>
        </sectionGroup>

This allows one to add the following to an application's app.config and override the machine wide setting (the default of 10 minutes or whatever setting is defined in the machine.config.):

<system.transactions>
<machineSettings maxTimeout="00:20:00" />
    </system.transactions>

Although this approach changes a machine wide setting, it does not change the machine wide maxtimeout for the machine.  One will be able to retain whatever value is set for the maxTimeout and one will be able to set whatever value is fit for the specific application in the app.config.  Thus, each app can override the machine wide maxTimeout setting and set its own maxTimeout.  I think this is better than the first approach as it is a bit more secure.  It doesn't change the machine wide maxTimeout setting which changing could consequently expose a DOS situation for other apps that may be on the server.  There may be a company policy that doesn't allow this option as it would give any application the ability to change the setting.

3.) Use reflection to change the setting via custom code.  This approach accesses private data members of Microsoft classes and thus could break in the future.  It doesn't require modifying the machine.config,  it doesn't open up other application to possible DOS situations and it circumvents any company policy.  Below is the code that does this:

    public static class TransactionManagerHelper
    {
        public static void OverrideMaximumTimeout(TimeSpan timeout)
        {
            //TransactionScope inherits a *maximum* timeout from Machine.config.  There's no way to override it from
            //code unless you use reflection.  Hence this code!
            //TransactionManager._cachedMaxTimeout
            var type = typeof(TransactionManager);
            var cachedMaxTimeout = type.GetField("_cachedMaxTimeout", BindingFlags.NonPublic | BindingFlags.Static);
            cachedMaxTimeout.SetValue(null, true);

            //TransactionManager._maximumTimeout
            var maximumTimeout = type.GetField("_maximumTimeout", BindingFlags.NonPublic | BindingFlags.Static);
            maximumTimeout.SetValue(null, timeout);
        }
    }

To use it call the following before creating the TransactionScope object:

TransactionManagerHelper.OverrideMaximumTimeout(TimeSpan.FromMinutes(5));

I've tested all three options.  They will all work.  I also verified that using the reflection method will change the setting for just the one application and not for other applications on the same server.


I propose using the reflection method and using a custom appSetting from the app.config to specify the timeout value passed to OverrideMaximumTimeout.  Another option would be to re-use the System.Transactions.Configuration.DefaultSettingsSection configuration section.  If a Timeout value exists for that configuration section in the app.config then pull the value from there and use it in the call to OverrideMaximumTimeout.

Thursday, September 12, 2013

Deploying Signed PowerShell Scripts in the Enterprise the Free and Easy Way

Some time back I created a PowerShell script that goes through all the computers you have in a list, searches those computer's certificate store for expired/expiring certificates and emails you reminders.  The script was designed to run nightly on one central computer. 

Side note: I have a quite a few certificates from StartSSL installed on a bunch of computers.  If you've never heard of StartSSL, I highly recommend checking them out!  You can get free SSL certificates and they're trusted by pretty much all computers these days.

I had a problem with the script I wrote though.  It required that the security context in which the PowerShell script ran had access to all the remote computer's certificate store.  I didn't like this for a couple reasons.  First, there was no central AD group that allows for this.  Which makes sense if you think about it.  Sure you could run it with domain administrator access but you shouldn't be doing that.  Following the Principal of Least Privilege, you want to limit access to the most essential.  So I thought add the user account that runs the PowerShell script to the "Certificate Service DCOM Access" on each individual machine that I wanted to get certificate reminders for.  This seems bad to me.  If that user account gets compromised the attacker has access to all the certificates on all the other computers.  Also the attacker would have access to modify all those certificate stores. 

After talking it over with some people, I decided the best route was to modify the PowerShell script slightly and have it execute on every computer instead of one central computer.  The script would just check its own certificate store and email about its own certificates.  This way the user account running the script doesn't need access to all the other computer's certificate store.  The problems I saw with this approach was that we'd need to change the PowerShell execution policy on all the computers from the default of not allowing scripts to either unrestricted or signed.  Unrestricted execution was out of the question, AllSigned it was.  So off I set to figure out how to sign PowerShell scripts and deploy everything via GPO to make like easier.

I've concluded that there are three approaches to signing scripts.  Using an internal certificate server, using a 3rd party certificate server and manually creating your own certificates. 

There are a ton of articles on how to do it with your own internal CA or a 3rd party CA but I couldn't find one about the manual method.  I didn't have an internal certificate server, so that right away was kind of out of the question.  I wasn't looking to implement one at this time.  Although I'd like to in the future. 

So I started looking at 3rd party CAs.  Specifically I started looking at using StartSSL code signing cert that you get with their class 2 validation level.  I got things working with this but I was worried about what would happen after two years when the code signing certificate expires.  I knew that I could use a timestamp server when signing my code to extend the validity of the script past the certificate's lifetime but when I tried testing it, it didn't seem to work?  Exploring this some more, it turns out that I would need to get StartSSL's extended validation to be able to time stamp with a code signing certificate from StartSSL.  Long story short, the certs created by StartSSL have the Lifetime Signing (1.3.6.1.4.1.311.10.3.13) OID added to them unless you get the extended validation and this OID prevents time stamping from working.  Check out this forum if you'd like a further explanation.

Side note:  They should have called it "No LifeTime Signer" or something a little more intuitive.

I didn't want to spend the money to upgrade to the extended validation level and I didn't want to spend much more time on this.  So I decided to explore the manual route quickly and it turned out to be the best option.  The certificate expires in 2039 and everything can get deployed via GPO to make life easy.  So here are the steps I took.  I'll break it down into 4 broad parts: 

  1. Modifying My Existing PowerShell Script and create Batch File
  2. Creating the Certificates
  3. Signing the PowerShell Scripts
  4. Deploying Everything

Modify the Existing PowerShell Script and Create a Batch File
The original PowerShell script that emails certificate reminders can be found here. I changed a couple lines to have it run on all the computers instead of one.

Change the line:
$Computers = "server1", "server2", "server3" #list of servers to check
To:
$Computers = $env:COMPUTERNAME

This basically keeps the script as is but runs it with only itself in the list of computers.  I also changed the email subjects and body slightly to include friendly names on the certificates.  Here is the modified script, copy it and save it as ExecuteCertReminder.ps1.  Be sure to set your own SMTP server and email addresses in the script.

#this script is intended to be run on one computer, it should then be pushed out with GPO and scheduled to run on each computer
$Computers = $env:COMPUTERNAME #list of computer names "server01", "server02"
$SmtpServer = "smtpserver.domain.com"
$MailFrom = "mailfrom@domain.com"
$MailTo = "mailto@domain.com"

# Change above settings. No need to change anything below.
$DaysToExpire = 30   
# increase $DaysToExpire if you want more of a warning, decrease it if you would like less
$StoreName = "My" 
# change $StoreName above to one of the valuse below if you wish to check a store other then the personal store, for example if you're worried about intermediate CA certs expiring
# "AddressBook", "AuthRoot", "CertificateAuthority", "Disallowed", "Root", TrustedPeople", "TrustedPublisher"

$deadline = (Get-Date).AddDays($DaysToExpire)

ForEach ($c in $Computers) { 
    Try { 

        $store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\$c\$StoreName","LocalMachine")

        $store.open("ReadOnly")

        # all certs
        #$store.certificates | Select *
        
        # will expire certs
        $store.Certificates | ? {$_.NotAfter -le ($deadline-and $_.NotAfter -ge (Get-Date)} | Select *, @{Label="ExpiresIn"; Expression={($_.NotAfter - (Get-Date)).Days}} | % {

            $ExpiresIn = $_.ExpiresIn
            $NotAfter = $_.NotAfter
            $friendlyNAme = $_.FriendlyName

            $emailBody = "The following certificate on $c will expire in $ExpiresIn days on $NotAfter" + [System.Environment]::NewLine
            $emailBody += $_ | select * | Out-String

            Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certificate about to Expire on $c - $friendlyNAme" -Body $emailBody

            Write-Host $emailBody
        }
           
        # expired certs
        $store.Certificates | ? {$_.NotAfter -lt (Get-Date)} | Select *, @{Label="ExpiredOn";Expression={$_.NotAfter}} | % {

            $ExpiredOn = $_.ExpiredOn
            $friendlyNAme = $_.FriendlyName

            $emailBody = "The following certificate on $c expired on $ExpiredOn" + [System.Environment]::NewLine
            $emailBody += $_ | select * | Out-String

            Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "CERTIFICATE EXPIRED on $c - $friendlyNAme" -Body $emailBody

            Write-Host $emailBody
        }

    }
    Catch {            
        $emailBody = "Errors detected during certification reminder powershell script execution. " + [System.Environment]::NewLine + [System.Environment]::NewLine + "$($c): $($error[0])" 

        Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certification Reminder Powershell Script Execution Error" -Body $emailBody

        Write-Host -foregroundcolor Yellow $emailBody #"$($c): $($error[0])" 
    } 

    Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "ExecuteCertReminder ran on $c " -Body "ExecuteCertReminder ran on $c "
}

Next create a batch file called ExecuteCertReminder.bat containing the following:

powershell.exe -ExecutionPolicy Bypass -Command ". '\\network path will be determined later\ExecuteCertReminder.ps1'"

We'll use this batch file later when we deploy everything with GPO and create a scheduled task.  Note that we're going to deploy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat with the GPO we create later.  They'll be stored in the domain's SysVol alongside the GPO we create later.  Thus we'll be coming back and editing this batch file once we know the path.

Creating the Certificates
I followed Scott Hanselman's blog about signing PowerShell scripts to create the certificates.  I modified his stuff slightly.

First go download the Windows SDK and install it on your workstation to gain access to the makecert executable.

On your workstation, open up the SDK Command Prompt with administrator privileges and run the slightly modified command from Scott's blog:

makecert -n "CN=Your Company Name PowerShell Certificate Root" -a sha256 -eku 1.3.6.1.5.5.7.3.3 -r -sv root.pvk root.cer -ss Root -sr localMachine

A dialog should pop up asking for a password.  Create a password and remember it.


Enter the password you created in the next dialog box.


This creates a public and private key root cert pair that can only be used to make code signing certs and puts the public key in your localmachine root store.  It creates two files.  One with the public key and one with the private key.  Keep these two files some place secure.  You will need them to sign other certificates in the future.  Later we will take this certificate and deploy it to the root certificate store on all your computers in your domain.  Do not lose the private key file if you intend to create other certs later on with this root cert.

Next run the slightly modified command from Scott's blog:

makecert -pe -n "CN=Your company Name PowerShell Signing Cert" -ss MY -a sha256 -eku 1.3.6.1.5.5.7.3.3 -iv root.pvk -ic root.cer

A dialog will pop up asking for the password you created in the previous step.


This creates the certificate pair that you will using for signing your PowerShell scripts and puts it in your personal certificate store.  It's marked as exportable.  You'll want to export it to a PFX file and keep it secure. 

Open the MMC by tying "mmc" and hitting enter at a command prompt.  Once loaded, select File, Add/Remove Snap-In. A dialog opens.


Select "Certificates" and click Add.


Make sure the "My user account" radio is selected and click Finish.  Click Ok on the "Add or Remove Snap-Ins" dialog.

Expand "Certificates - Current User", Expand "Personal" and Select "Certificates".


Find the certificate you created in the right hand pane, select it and right click.  Select "All Tasks", "Export".

An export wizard comes up, click Next.


Select "Yes, export the private key" radio and click next.


Check the "Export all extended properties" check-box and click next.


Check the "Password" check box, enter a password and click Next.



Enter a name for the exported certificate file with a .pfx extension and click Next.  A summary dialog will appear, click Finish and the export should be complete.  You should not have three files.  The root private key, the root public key and the .pfx file containing both the signing certificates key pair.  Key these three files some place safe where no one will have access to them.

Signing the PowerShell Scripts
You could sign the scripts manually but I found a link that showed how to add a menu item to the PowerShell Integrated Scripting Environment.  For me, this simplifies the process of signing scripts.  Once it's setup it's easy to use.  I won't repost the information, please follow the link and read the blog about how to do it.  

I will summarize the process though. 
  1. Download the script from the blog.

  1. The file has a txt extension.  Rename the file to Sign-ISEScript.ps1.

  1. If you have multiple code signing certs you may need to modify this file to select the correct certificate.
  1. To check if you have multiple code signing certificates, run the following from the interactive shell at the bottom of the ISE application:
    Get-ChildItem -Path cert:\CurrentUser\My -CodeSigningCert

  1. If only one certificate is listed, you can go on to step 4.
  2. If more than one certificate is listed, make note of the thumbprint of the certificate you want to use.
  3. You will need to change a line in Sign-ISEScript.ps1 to select the proper certificate.  Find the following line in Sign-ISEScript.ps1:
$cert=get-childitem -Path cert:\currentuser\my -CodeSigningCert

And replace it with

$cert=Get-ChildItem -Path cert:\CurrentUser\My -CodeSigningCert  | where {$_.Thumbprint -eq 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' }

Replace the F's with the thumbprint that you noted earlier.

  1. Edit your ISE profile
    1. In the interactive shell at the bottom, type "psedit $profile".  A tab will open called Microsoft.PowerShellISE_profile.ps1.
    2. Add the following to this tab

      . C:\<path to downloaded file>\Sign-ISEScript.ps1
      $psISE.CurrentPowerShellTab.AddOnsMenu.submenus.Add(“Sign Script”,{Sign-ISEScript},$null) | Out-Null

    3. Save the  file and reopen ISE.  There should now be a menu item called "Sign Script" under the "Add-ons" menu.
       
  1. Open the file you want to sign (ExecuteCertReminder.ps1 in this case), select Add-ons, sign-script.  It should sign your script by adding a signature to the end of the file, save it and reopen.


Deploying Everything
The final major hurdle is to push everything out to the computers you want the script to run on.  I'm not going to get into GPO organization strategies or anything.  I'm just going to setup one GPO with all the settings required.  You can then link this GPO however you want to any OU you want.  Also, you can break this out into multiple GPOs if you so desire.  There are three basic things this GPO needs to do.

  1. Push out the appropriate certificates
  2. Set the PowerShell execution policy to AllSigned
  3. Setup a scheduled task to execute the PowerShell script

Start by opening the Group Policy Management snap-in either on a domain controller or your workstation if you have the remote admin tools installed.  I assume you know how to make a new GPO.   You'll want to edit The GPO.  In the Group Policy Management Editor:

Right click Policy Object Name/Computer Configuration/Policies/Windows Settings/Security Settings/Public Key Policies/Trusted Root Certification Authorities and select import.  Use the root.cer file you created earlier.


Right click Policy Object Name/Computer Configuration/Policies/Windows Settings/Security Settings/Public Key Policies/Trusted Publishers and select import.  Use the "signing cert.pfx" file you exported earlier.

That's all you need to do to push out the certificates.

Continue editing the GPO.  Navigate to Policy Object Name/Computer Configuration/Policies/Administrative Templates/Windows Components/Windows PowerShell.  Double click "Turn on Script Execution" in the right hang pane.  Check the Enabled radio and change the drop down to "Allow only signed scripts".  Click Ok.



Now you'll need to setup the scheduled task to execute nightly.  First we'll copy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat to the corresponding GPO in the SysVol on the domain.  In the Group Policy Management Snap-in find your new GPO and click on the details tab in the right hand pane. 


Note the GUID used for the Unique ID, yours will be different than the one above.  Open windows explorer and navigate to the SysVol share on one of your domain controllers.  There should be a subfolder with your domain name and under that a Policies folder.  Open this policies folder then find and open the folder with the GUID noted previously.  Then navigate farther to Machine\Scripts.  Copy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat this this network location. 

Make note of the full path to the PowerShell script.  You'll need to edit the batch file and enter the full network path to the PowerShell script.

Make note of the full path to the batch file.  We'll need the full path when creating the scheduled task in GPO.

Back in the In the Group Policy Management Editor navigate to Policy Object Name/Computer Configuration/Preferences/Control Panel Settings/Scheduled Tasks.  Right click -> New -> Scheduled Task.  A dialog opens up. 
Set Action to Replace. 
Enter a Name of ExecuteCertReminder.
Enter the entire path to the batch file in the Run textbox.


Click on the schedule tab and setup whatever schedule you would like.  Hit Ok and you should be done.

Monday, August 26, 2013

IIS/ASP.NET Windows Authentication Fails After Changing a User Name in Active Directory

We ran into a problem were a user's login changed because their last name changed.  They were able to login to the website with their old user name but the new one would not work.  It turns out that the local security account (LSA) on the front end web servers had their old user name cached.

We followed the guidance from Microsoft http://support.microsoft.com/kb/946358.
Once we added the DWORD registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\LsaLookupCacheMaxSize with a value of 0 things started working.  Once this was done on all the front end web servers we removed the registry key so we would get the performance gains from caching again.

Thursday, February 14, 2013

ASP.NET Web API with HealthMonitoring

After deploying a ASP.NET Web API app it was realized that exceptions were not being reported via the built in Health Monitoring features provided by ASP.NET.  The following shows how to get the healthMonitoring element to report on exceptions thrown inside a Web API controller.  It was inspired by Andrew Wilinski's post about getting ASP.NET MVC's HandleErrorAttribute to invoke the built in health monitoring features.

Of note, it appears that the HandleErrorAttribute in MVC 4.0 now invokes the health monitoring features and Andrew's fix is not required for the MVC controllers you're used to using.  ASP.NET Web API on the other hand uses a different set of exception filters then ASP.NET MVC and thus we need a slight variant of Andrew's fix to get health monitoring working with it.  Similar to how this feature wasn't implemented by Microsoft at first, I'm sure that one of the subsequent versions of the Web API code will hook into the health monitoring.

The following should allow exceptions thrown in a ASP.NET Web API controller to be reported by Health Monitoring.


Create the following two classes in a file called HealthMonitor.cs:


    public class WebRequestErrorEventWebApi : WebRequestErrorEvent
    {
        public WebRequestErrorEventWebApi(string message, object eventSource, int eventCode, Exception exception) : base(message, eventSource, eventCode, exception) { }

        public WebRequestErrorEventWebApi(string message, object eventSource, int eventCode, int eventDetailCode, Exception exception) : base(message, eventSource, eventCode, eventDetailCode, exception) { }
    }

    public class ExceptionFilterWebApiAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext actionExecutedContext)
        {
            base.OnException(actionExecutedContext);

            new WebRequestErrorEventWebApi("An unhandled exception has occurred.", this, 103005, actionExecutedContext.Exception).Raise();
        }
    }


You'll need the following using statements:

using System.Web.Http.Filters;
using System.Web.Management;


Then make sure the following is called from within Application_Start in global.asax.cs or refactor the code into WebApiConfig.Register (the WebApiConfig class is created in the App_Start directory by the web api website template)

GlobalConfiguration.Configuration.Filters.Add(new ExceptionFilterWebApiAttribute());



As long as you've configured your health monitoring properly, you should be all set.

Monday, January 21, 2013

Windows Certificate Expiration Email Reminders


Have a ton of windows servers and a ton of certificates on those servers?  Are those certificates expiring before you remember to renew them?  Schedule this powershell script to run daily and it will check the servers you specify and email you when the certificates on those servers are about to expire.

Modify the first four variables of the following powershell script to your environment's setup and schedule the it to run daily.  It should send you an email 14 days in advance of a certificate expiring on any of the machines listed in the $Computers variable.  It will also email about expired certificates.

# ******************** script starts here *************************
$Computers = "server1", "server2", "server3" #list of servers to check
$SmtpServer = "smtpserver.domain.com"
$MailFrom = "mailfrom@domain.com"
$MailTo = "mailto@domain.com"

# Change above settings.  No need to change anything below.
$DaysToExpire = 14  
# increase $DaysToExpire if you want more of a warning, decrease it if you would like less
$StoreName = "My"
# change $StoreName above to one of the valuse below if you wish to check a store other then the personal store, for example if you're worried about intermediate CA certs expiring
# "AddressBook", "AuthRoot", "CertificateAuthority", "Disallowed", "Root", TrustedPeople", "TrustedPublisher"

$deadline = (Get-Date).AddDays($DaysToExpire)

ForEach ($c in $Computers) {
    Try {

        $store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\$c\$StoreName","LocalMachine")

        $store.open("ReadOnly")

        # all certs
        #$store.certificates | Select *
       
        # will expire certs
        $store.Certificates | ? {$_.NotAfter -le ($deadline) -and $_.NotAfter -ge (Get-Date)} | Select *, @{Label="ExpiresIn"; Expression={($_.NotAfter - (Get-Date)).Days}} | % {

            $ExpiresIn = $_.ExpiresIn
            $NotAfter = $_.NotAfter

            $emailBody = "The following certificate on $c will expire in $ExpiresIn days on $NotAfter" + [System.Environment]::NewLine
            $emailBody += $_ | select * | Out-String

            Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certificate about to Expire on $c" -Body $emailBody

            Write-Host $emailBody
        }
         
        # expired certs
        $store.Certificates | ? {$_.NotAfter -lt (Get-Date)} | Select *, @{Label="ExpiredOn";Expression={$_.NotAfter}} | % {

            $ExpiredOn = $_.ExpiredOn

            $emailBody = "The following certificate on $c expired on $ExpiredOn" + [System.Environment]::NewLine
            $emailBody += $_ | select * | Out-String

            Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "CERTIFICATE EXPIRED on $c" -Body $emailBody

            Write-Host $emailBody
        }

    }
    Catch {          
        $emailBody = "Errors detected during certification reminder powershell script execution. " + [System.Environment]::NewLine + [System.Environment]::NewLine + "$($c): $($error[0])"

        Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certification Reminder Powershell Script Execution Error" -Body $emailBody

        Write-Host -foregroundcolor Yellow $emailBody #"$($c): $($error[0])"
    }

}