Tuesday, 23 February 2021

Quick Continuous Integration/Deployment (CI/CD) workflow for Optimizely DXP using Azure DevOps

There are a lot of questions on Episerver World on how to quickly deploy the code on Episerver DXP or more specifically how to set up Continuous Integration/Deployment (CI/CD) workflow for Episerver DXP. 

Episerver recommends using Development API to support a CI/CD workflow. You can generate the API credential through the DXP Service management portal (https://paasportal.episerver.net) and while we are focusing our effort around Azure DevOps you can use the EpiCould Powershell module to wraps the functionality to integrate with a CI/CD pipeline. 


This feature from Episerver is game-changing and powerful which gives you the ultimate power to set up your CI/CD workflow as per your project requirements. With all the Development API greatness it comes with a catch, the deployment takes forever.  This is roughly the same time the deployment takes from Integration to Preproduction on DXP when you do the code deployment manually from the DXP Service management portal. 



This is sometimes not acceptable when developers are doing frequent pull requests against the development branch (or you don't bother with PRs and directly pushing your code in the development branch anyway) and the change needs to be immediately available in the integration environment for testing. Waiting for 20-40 minutes for the deployment to complete its not an option in this case, so how can we do this more quickly? 

There is another approach that is not common knowledge and using this approach we can be done with deployment within 5 minutes or less. Sound interesting? let's explore this and here is my take on it how to set up CI/CD workflow properly.  

I named this approach "Configuring CI/CD Pipelines as Code with YAML using Service Connection" (you can call this whatever you like). I find this method most convenient and resourceful for my CI/DI workflow against serval of my projects. The drawback of this approach is the deployment can only be done in the integration environment. You still have to rely on Deployment API or manual deployment through the DXP Service management portal for preproduction and production environments.  

The reason is we to rely on Service connections for this approach to work and for that, we need service principal credentials from Episerver. Episerver only provides this information for the integration environment. 

I have set up this process against serval of my projects and deployment to integration maximum take five minutes (including running the unit tests).

Now we have a method lets set it up against the Alloy project with 5 easy steps.

Step 1: Get Credentials

Send an email to Episerver Support (support[at]episerver.com) and ask them to generate the following information for the integration environment. Don't forget to include the project name in the email. 

  • Subscription Id
  • Subscription Name
  • Service Principal Id
  • Service principal key
  • Tenant Id
Once you have this information we proceed with step 2. 

Step 2: Setup Service Connection in DevOps

In Azure DevOps go to  Project Setting and click "Service Connections"


In the top right corner click "New service connection"


Select "Azure Resource Manager" and click next


Select "Service principal (manual)" and click next 


Fill in all the information provided by Episerver Support. Name your service connector name and click verify and save.  


Step 3: Changes in  Visual Studio Project 


After setting up the service connector, we have to make some changes to our Visual Studio Project for the correct web.config transformation. In this example project, we have three environments specific configuration files as per Episerver documentation
  • web.integration.config
  • web.preproduction.config
  • web.production.config
and they are set up as follow in the web project file

  <Target Name="DXCConfigs">
    <Delete Files="web.integration.out.dxc.config;web.preproduction.out.dxc.config;web.production.out.dxc.config" />
    <TransformXml Source="web.config" Transform="Web.integration.config" Destination="web.integration.out.dxc.config" />
    <TransformXml Source="web.integration.out.dxc.config" Transform="web.preproduction.config" Destination="web.preproduction.out.dxc.config" />
    <TransformXml Source="web.preproduction.out.dxc.config" Transform="web.production.config" Destination="web.production.out.dxc.config" />
  </Target>

Now add the following at the end of the project file so we have the correct web.config after the transformation and ready for our CI/CD pipeline and we name this adobuild.
  <Target Name="adobuild">
    <TransformXml Source="web.config" Transform="Web.integration.config" Destination="web.config" />
    <CallTarget Targets="Build" />
  </Target>


Step 4: The YAML Pipeline

Everything is ready for ur pipeline, let's create one in DevOps. We are going to trigger this pipeline against the development branch. It means every time a code check-in will happen against the development branch this pipeline will automatically run and deployment the code on the integration environment.

In DevOps click Pipelines 

Click "New pipeline" in the top right corner 

After going through the wizard by selecting

  • Connect 
  • Select
  • Configure 
you will end up with the example YAML 

Let's replace the content of the YAML file with the following 

# ASP.NET

# Build and test ASP.NET projects.
# Add steps that publish symbols, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4

trigger:
  batch: true
  branches:
    include:
    - development
  
pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:

- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    command: 'restore'
    restoreSolution: '**/*.sln'
    feedsToUse: 'config'
    nugetConfigPath: './nuget.config'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    msbuildArgs: '/target:adobuild /p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    clean: true

- task: VSTest@2
  inputs:
    testAssemblyVer2: |
     **\*Test*.dll
     !**\obj\**
    platform: '$(BuildPlatform)'
    configuration: '$(BuildConfiguration)'

- task: AzureRmWebAppDeployment@4
  inputs:
    ConnectionType: 'AzureRM'
    appType: 'webApp'

    packageForLinux: '$(build.artifactStagingDirectory)/WEB-PROJECT-NAME.zip'
    azureSubscription: 'SERVICE-CONNECTOR-NAME'
    WebAppName:  INTEGRATION-ENVOIRMENT-NAME
    ResourceGroupName: INTEGRATION-ENVOIRMENT-NAME
    deployToSlotOrASE: true
    SlotName: 'production'
    enableCustomDeployment: true
    RemoveAdditionalFilesFlag: true

As you can see I have made some amendments to the example YAML syntax generated by DevOps.

So as you can see in the Trigger task I have set up a development branch, which means this pipeline will automatically trigger for the development branch if there are any code commits happens. 

In the VSBuild task, I have added the adobuld that we set up in step 3.

The task AzureRmWebAppDeployment is the main task where actually deployment is done on the Episerver DXP integration environment. 

There are certain values need to be filled in this section that I highlighted in bold.

  • WEB-PROJECT-NAME : This is the web project name in your Visual Studio solution.
  • SERVICE-CONNECTOR-NAME : The name of the service connector we created in step 2
  • INTEGRATION-ENVOIRMENT-NAME: The integration environment hostname you can get from DXP Service management portal  e.g alloy01mstr2bx1u3inte

Step 5: Test

Now we have everything we need its time to test this process. Just commit a code change in your development branch and the pipeline will take care of the rest.
February 23, 2021 →

Monday, 15 February 2021

Simple Ways to Improve Optimizely Websites Security

In my previous post, Basic Episerver Coding Best Practices I briefly mentioned the website security.  I thought I could go a little bit in details about basic security measure you can take in your ASP.NET website or in particular Episerver website. 

Each project comes with its own set of security requirements but I usually start from the following list to cover the basics. Keep in mind the list below are just suggestions and you can always opt-out or include other measures to quench your thirst for your website security. 

Before we go into detail lets do a quick security check or your website. I usually use these tools (https://www.immuniweb.com/websec/ & https://securityheaders.com/)  but there are so many free tools available to quickly give you a sanity check of your website security. 

Episerver has a suggestive security checklist for the websites (https://world.episerver.com/documentation/developer-guides/CMS/learning-path/security-checklist/). Some of the suggestions in this post are taken from the checklist.

Use HTTPS

Implement Rewrite Rule to ensure always redirect HTTP to HTTPS

   <rule name="HTTPS rewrite" stopProcessing="true">
        <match url="^(.*)$" ignoreCase="false" />
        <conditions>
            <add input="{HTTP_X_FORWARDED_PROTO}" pattern="^http$" ignoreCase="false" />
        </conditions>
        <action type="Redirect" redirectType="Found" url="https://{SERVER_NAME}{URL}" />
    </rule>

Secure Cookies

<system.web>
    <httpCookies sameSite="Lax" httpOnlyCookies="true" requireSSL="true"/>
</system.web>

Using the Same-Site Cookie Attribute to Prevent CSRF Attacks

     <outboundRules>
        <clear />
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=lax" />
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>

You can read more about SameSite cookies at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite

Session Cookie Expires Attribute

The session cookie will be stored on the user's hard drive. An attacker or local user who has access to this cookie may be able to log into the application by resubmitting the session cookie.

The "Expires" attribute should not be set. This will cause the cleanup management of the browser to be used for deleting the cookie once the browser is closed.


Turn off Detailed Errors and Set Compilation Mode to Release


<compilation debug="false" />
<episerver.framework>
    <clientResources debug="false"/>
  </episerver.framework>

JavaScript Code Obfuscation

The JavaScript code is human-readable. An attacker is therefore able to analyze the source code to find vulnerabilities. Confidential details can also be revealed through the source code (e.g., developer information or source code comments).

Make sure that JavaScript code should be automatically obfuscated on deployment.

For manual obfuscation take a look at this online tool https://obfuscator.io/

Implement Server-Side Input Validation

Input that is not correctly validated can trigger unexpected behaviour in the backend application, which might lead to security vulnerabilities.

Any input for the application should be checked to verify that it only contains valid data.

Improving Security Using Custom Headers

The following headers should be implemented to improve the security in Episerver project


X-Frame-Options

The X-Frame-Options header ensures that hackers don't iframe the site, in order to trick the user into clicking links which they never intended to. If we are using ASP.NET MVC 5 or newer, this header is added automatically. 

Adding the header in previous versions or other web frameworks is easy using web.config:

  
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="X-Frame-Options" value="SAMEORIGIN"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

If we are using iframes on the same domain, we can change the value to SAMEORIGIN.


X-Xss-ProtectionX-ASPNET-VERSION

The X-Xss-Protection is a feature implemented in a most modern browser, which will stop loading the page when a cross-site scripting attack is detected. 

Adding the header happens through web.config as well:

<system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="X-XSS-Protection" value="1; mode=block"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

X-Content-Type-Options

To avoid MIME type sniffing, we can add the X-Content-Type-Options header. This makes it harder for hackers to guess the right mime type, by inspecting the content. Adding the header is easily done through web.config:

 
 <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="X-Content-Type-Options" value="nosniff "/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Referrer-Policy

Browsers automatically add the Referer header, when a user clicks a link on the site. This means that a linked website will be able to see where the users are coming from. While this is a great feature for Analytics, we may have sensitive information in the URLs, which we don't want to forward to other domains. 

To remove the referrer entirely, add the following header to web.config:

 
 <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Referrer-Policy" value="strict-origin"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

In real life, we may want another value for Referrer-Policy. Removing the referrer entirely makes it impossible to see the internal traffic flow on the website. Check out Referrer-Policy on mozilla.org for a list of possible values.


X-Permitted-Cross-Domain-Policies

To restrict Flash components to make cross-origin requests, we should disable it entirely (unless we are using Flash of course). 

To do so, add the X-Permitted-Cross-Domain-Policies to web.config:

<system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="X-Permitted-Cross-Domain-Policies" value="none"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Strict-Transport-Security

After implementing implemented HTTPS on the website, we can prevent any communication happening over HTTP using the Strict-Transport-Security header:

  
 <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

The max-age value tells browsers to use this setting for the specified number of seconds. In this case a year. The includeSubDomains part can be excluded if we are hosting non-HTTPS websites on subdomains.


X-Powered-By

The X-Powered-By header is automatically added by ASP.NET. To make it less obvious which technology we are using to host our website, we should remove this header through web.config:

 
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

X-AspNetMvc-Version

Much like X-Powered-By, X-AspNetMvc-Version is a header automatically added by the framework. To avoid telling hackers that we use MVC and which version, we should remove it. X-AspNetMvc-Version cannot be removed through web.config, but we can disable it from code. 

Add the following to the Global.asax.cs:

public class EPiServerApplication : EPiServer.Global
{
	protected void Application_Start()
	{
		 MvcHandler.DisableMvcResponseHeader = true;
	}
}

 
<system.web>
	 <httpRuntime enableVersionHeader="false" />
</system.web>

Server

ASP.NET also reveals the server hosting the application. If a hacker knows that we are using IIS, this narrows the number of weaknesses that he/she needs to try. 

To remove the Server header, remove it from code in either a filter or through Global.asax.cs:

public class EPiServerApplication : EPiServer.Global
{
   protected void Application_PreSendRequestHeaders()
   {
       if (HttpContext.Current != null)
       {
           HttpContext.Current.Response.Headers.Remove("Server");
       }
   }
}

Feature-Policy

The Feature-Policy header is a recent addition to the range of security-related headers. When specifying the header, we tell the browser which features our site uses or not. This is a great feature, especially if we embed other websites. 

To add the header, make the following change in web.config:

 
   <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Feature-Policy" value="accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'"/>
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Content-Security-Policy

Set basic Content-Security-Policy header values that enforce https:// for external styles and scripts and allows inline scripts in the website.

  
<system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Content-Security-Policy" value="default-src * data:; script-src https: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'"  />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Disable HTTP TRACE Method

The HTTP TRACE method is designed for diagnostic purposes. If enabled, the web server will respond to requests that use the TRACE method by echoing in its response the exact request that was received.

This behaviour is often harmless but occasionally leads to the disclosure of sensitive information such as internal authentication headers appended by reverse proxies. This functionality could historically be used to bypass the HttpOnly cookie flag on cookies, but this is no longer possible in modern web browsers.

  <system.webServer>
    <security>
      <requestFiltering>
        <verbs>
          <add verb="TRACE" allowed="false" />
        </verbs>
      </requestFiltering>
    </security>
  </system.webServer>

References:
February 15, 2021 →