Wednesday, April 3, 2013

How to disable CMS edit/admin mode on "public facing" servers in a load balanced environment

Update: some people have pointed out that they prefer the method of disabling the specific virtual paths in episerver.config.  See the end of this blog post for more information.

Hi Folks!

I realize that there have been several other blog posts regarding this topic, but after lots or trial and error I believe I have found a solid approach for disabling CMS edit/admin mode on public facing IIS servers in a load balanced environment.

There are other techniques that can be used to secure CMS edit/admin modes in a single server environment.  Check out this blog post by David Knipe for additional information.  It essentially uses IP white lists at the IIS level.  Maintaining IP whitelists can be problematic, so I prefer having a dedicated CMS server that's only accessible within the internal network.

In my last blog post we covered how to setup EPiServer load balancing using net.tcp.  We also detailed the three basic models that can be used when implementing EPiServer load balancing.  Today we'll be focusing on the Security model.

To recap, the security model provides a dedicated server for CMS editors to connect to and make changes to CMS content.  This server is normally only accessible from within the company's internal network.  The one or more "public facing" servers are what the general public connects to.  These "public facing" servers have the CMS edit/admin mode completely disabled.



For example:

Let's say we have our CMS server available onon http://cms.mysite.com (internal network only), and our "public facing" server available on http://www.mysite.com, then...

  • http://cms.mysite.com/episerver would prompt for login, and if the credentials are correct, allow the individual to navigate to CMS edit/admin mode.
  • http://www.mysite.com/episerver would be completely disabled, even if the user managed to steal a CMS editor's credentials.
Some of my Swedish colleagues have told me that this sort of setup is not very common in Sweden.  But I've personally noticed that many of our clients here in the States prefer having a dedicated CMS editor server.  I can't say I blame them.

So how do we harden the "public facing" servers.  It's remarkably easy.  All you have to do is make some tweaks to the Web.config file on the public facing server(s).

Disclaimer: these instructions work for EPiServer CMS 6 R2, I'm not sure about EPiServer CMS 7, but I believe the premise is essentially the same thing.

Web.config changes

The idea here is to modify any <location> element in the Web.config which currently requires WebEditorsWebAdmins or Administrators role membership to access.

For example, the <location> element for "/episerver" access by default looks like:

<location path="episerver">
  <system.web>
    <httpRuntime maxRequestLength="1000000" />
    <pages enableEventValidation="true">
      <controls>
        <add tagPrefix="EPiServerUI" namespace="EPiServer.UI.WebControls" assembly="EPiServer.UI" />
        <add tagPrefix="EPiServerScript" namespace="EPiServer.ClientScript.WebControls" assembly="EPiServer" />
        <add tagPrefix="EPiServerScript" namespace="EPiServer.UI.ClientScript.WebControls" assembly="EPiServer.UI" />
      </controls>
    </pages>
    <globalization requestEncoding="utf-8" responseEncoding="utf-8" />
    <authorization>
      <allow roles="WebEditors, WebAdmins, Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
  <system.webServer>
    <handlers>
      <clear />
      <add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" />
      <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" modules="ManagedPipelineHandler" scriptProcessor="" resourceType="Unspecified" requireAccess="Script" allowPathInfo="false" preCondition="integratedMode" responseBufferLimit="4194304" />
      <add name="SimpleHandlerFactory-Integrated" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" modules="ManagedPipelineHandler" scriptProcessor="" resourceType="Unspecified" requireAccess="Script" allowPathInfo="false" preCondition="integratedMode" responseBufferLimit="4194304" />
      <add name="WebServiceHandlerFactory-Integrated" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Services.Protocols.WebServiceHandlerFactory, System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" modules="ManagedPipelineHandler" scriptProcessor="" resourceType="Unspecified" requireAccess="Script" allowPathInfo="false" preCondition="integratedMode" responseBufferLimit="4194304" />
      <add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer" />
    </handlers>
  </system.webServer>
</location>

Notice that the <allow> element currently specifies that WebEditors, WebAdmins, and Administrators can access this location.  The <deny> elements tells IIS to deny everyone else.

All we essentially have to do is remove the <allow> element (comment it out) on the public facing servers and this location will be denied to everyone - including CMS editors/admins.  This means even if someone managed to steal the username and password of a CMS editor, they wouldn't be able to login to CMS edit mode unless they were somehow connected to the internal network and knew the URL to the CMS web application instance.

Please note: you can easily use Visual Studio transforms to create a Solution Configuration which automatically removes the <allow> element when publishing to the "public facing" servers.  We'll take a closer look at transforms in my next blog post.

Here we've commented out the <allow> element for the <location path="episerver"> element.

<location path="episerver">
  <system.web>
    <httpRuntime maxRequestLength="1000000" />
    <pages enableEventValidation="true">
      <controls>
        <add tagPrefix="EPiServerUI" namespace="EPiServer.UI.WebControls" assembly="EPiServer.UI" />
        <add tagPrefix="EPiServerScript" namespace="EPiServer.ClientScript.WebControls" assembly="EPiServer" />
        <add tagPrefix="EPiServerScript" namespace="EPiServer.UI.ClientScript.WebControls" assembly="EPiServer.UI" />
      </controls>
    </pages>
    <globalization requestEncoding="utf-8" responseEncoding="utf-8" />
    <authorization>
      <!--<allow roles="WebEditors, WebAdmins, Administrators" />-->
      <deny users="*" />
    </authorization>
  </system.web>
  <system.webServer>
    <handlers>
      <clear />
      <add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" />
      <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" modules="ManagedPipelineHandler" scriptProcessor="" resourceType="Unspecified" requireAccess="Script" allowPathInfo="false" preCondition="integratedMode" responseBufferLimit="4194304" />
      <add name="SimpleHandlerFactory-Integrated" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" modules="ManagedPipelineHandler" scriptProcessor="" resourceType="Unspecified" requireAccess="Script" allowPathInfo="false" preCondition="integratedMode" responseBufferLimit="4194304" />
      <add name="WebServiceHandlerFactory-Integrated" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Services.Protocols.WebServiceHandlerFactory, System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" modules="ManagedPipelineHandler" scriptProcessor="" resourceType="Unspecified" requireAccess="Script" allowPathInfo="false" preCondition="integratedMode" responseBufferLimit="4194304" />
      <add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer" />
    </handlers>
  </system.webServer>
</location>

You should repeat this step for all <location> objects that currently require WebEditors, WebAdmins, or Adminstrators role membership.

Depending on whether or not your using Commerce and/or Composer you might have to make the changes to the <location> elements with the following paths:

  • <location path="episerver">
  • <location path="episerver/CMS/admin" >
  • <location path="WebServices">
  • <location path="Admin/SettingsPlugin">
  • <location path="Admin/SitePlugin">
  • <location path="Edit">
  • <location path="EPiServerCommon">
  • <location path="dropit/plugin/extension/ui/edit">
  • <location path="dropit/plugin/extension/ui/admin">
After doing this, you'll notice one serious and annoying problem...




Assuming someone is able to login using CMS editor credentials, they can still access the EPiServer context menu (for on page editing), even if they cannot get to CMS edit/admin mode.

This is because the <location> elements don't affect access to the context menu.  The context menu is driven by EPiServer ACLs.

I stumbled across the solution on this blog post by Paul Houghton.  It requires building some code which will disable the EPiServer context menus for all page templates and then using appSettings to control whether or not this code should execute.  You could then configure the appSettings in Web.config for the servers where you want to disable the context menu appropriately.

Well appSettings was fine and dandy several years ago, but these days we use project level application settings.  See this post on Stackoverflow for addition information on the differences between appSettings and application settings.

So let's build a boolean application setting for our project in Visual Studio.



Now we can add some code in PageBase.cs (which all of our EPiServer templates inherit from) to override the onInit() page life cycle method.  This code checks the application settings value and determines if it should enable or disable the EPiServer context menu.

namespace MySite.Core
{
    public abstract class PageBase<T> : TemplatePage<T> where T : PageTypeBase
    {
        protected override void OnInit(System.EventArgs e)
        {
            if (ContextMenu != null && !Settings.Default.EnableEPiContextMenu)
            {
                ContextMenu.IsMenuEnabled = false;
            }

            base.OnInit(e);
        }
    }
}

The great part about application settings is that the values are compiled into the DLL, but can be overriden via the Web.config.  If you don't supply a value in Web.config, the compiled in value is used.  Pretty cool huh?

So now all we have to do is override the "True" value for our EnableEPiContextMenu in the Web.config on our public facing servers.

<applicationSettings>
  <MySite.Core.Properties.Settings>
    <setting name="EnableEPiContextMenu" serializeAs="String">
      <value>False</value>
    </setting>
  </MySite.Core.Properties.Settings>
</applicationSettings>

Extra steps for sites that don't require login functionality

If your site literally does not require any sort of login functionality for the general public you can further secure your "public facing" severs by disabling the login page completely.  You would keep the login page for the CMS server so that your CMS editors can login.

If you go this route, a person from the general public going to http://www.mysite.com/episerver would get a 404.  This is because:

  1. IIS determines the user is going to a location path that requires login.
  2. IIS tries to respond with the login page defined in the <forms> element in Web.config.
  3. IIS can't find the login page and therefore returns a 404.
If you are using the standard EPiServer login page, you can comment out this line in episerver.config.  This disables the virtual path to "~/Util" where the EPiServer login page is stored.

<virtualPath customFileSummary="~/FileSummary.config">
    <providers>
        ... more ...    
        
          
        <add name="App_Themes_Default" virtualPath="~/App_Themes/Default/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\App_Themes\Default"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />
            
        <add name="UI" virtualPath="~/episerver/CMS/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\UI\CMS"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />
            
        <!--<add name="UtilFiles" virtualPath="~/Util/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\util"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />-->

        ... more ...

    </providers>
</virtualPath>

If you are using a custom login page, just remove it from the build you publish to your "public" facing servers.

That about does it folks.  Tune in next time for how to use Visual Studio transforms to easily customize Visual Studio publish configurations.  This allows you to make configuration changes automatically when publishing builds of your web application to different servers: DEV, TEST, CMS, and PROD.

Thanks!

Rob

Update: As others have pointed out, another approach you can use is to remove the relevant virtual paths in episerver.config.  This approach will return a 404 when trying to access a resource that you don't want the general public to have access to.  This is because IIS literally can't find the resources (you disconnect the virtual link that points to the C:\Program Files\ directory on the file system).

I like the <location> element approach I outlined above works directly with IIS security, and I find it very simple to implement because it's easy to identify which <location> elements to modify.  But this approach is "closer to the metal" in that a malicious individual literally is unable to access the appropriate resources from the file system.

To implement the episerver.config virtual path approach, simply comment out the relevant virtual paths in episerver.config.

<!-- These virtual paths are commented out on 'public facing' server
to prevent access to CMS edit/admin mode

<add name="App_Themes_Default" virtualPath="~/App_Themes/Default/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\App_Themes\Default"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />

<add name="UI" virtualPath="~/episerver/CMS/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\UI\CMS"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />

<add name="UtilFiles" virtualPath="~/Util/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\util"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />

<add name="WebServiceFiles" virtualPath="~/WebServices/"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\webservices"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />

<add name="EPiServerCMS" virtualPath="~/episerver/CMS"
physicalPath="C:\Program Files (x86)\EPiServer\CMS\6.1.379.0\application\UI\EPiServer\CMS"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />

<add name="EPiServerShell" virtualPath="~/episerver/Shell"
physicalPath="C:\Program Files (x86)\EPiServer\Framework\6.2.267.1\Application\UI"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />

<add name="EPiServerUrlMappingVPP" virtualName="ExtensionMapping" virtualPath="~/episerver/CMS/"
bypassAccessCheck="false" showInFileManager="false" type="EPiServer.Web.Hosting.VirtualPathMappedProvider,EPiServer" />

<add name="EPiServerCommon" virtualPath="~/EPiServerCommon"
physicalPath="C:\Program Files (x86)\EPiServer\CommonFramework\4.1.517.380\Application\EPiServerCommon"
type="EPiServer.Web.Hosting.VirtualPathNonUnifiedProvider,EPiServer" />
-->