Dockerify your Sitecore 9.3 XP development environment - SSL, CM and Identity

Konabos Consulting Wins Multiple Sitecore Most Valuable Professional Awards

Dockerify: To move a program that was run on a monolithic application into a container system like Docker.

You hear Docker much more often in the Sitecore world nowadays mainly to simplify your development environment and reduce the complexity of installs. This was apparent in our recent project which involved Sitecore Commerce and the ease of setting up different devs with different pc specs on Docker for Sitecore.

I want to thank all the community and sitecore contributors for working on the Sitecore Docker repo - https://github.com/Sitecore/docker-images.

I am not an expert in using Docker. Initially I was extremely frustrated with Docker but what I can tell you is that, once you start using it in a real world example, it gets easier as you know more. The more you work with it the better you understand it.

Most of my frustration came from Docker for Windows, which I do not think is as stable as it could be and the public Sitecore for Docker repository. I also feel like people do not share their knowledge as much as they should. What is the point in guarding your knowledge?

I apologize for the long blog post. I have so much to share and I will blog a bit more about what helped me with Docker in future posts.

I suggest the following resources to get into Docker for Sitecore:

SSL

As we were working with Sitecore Commerce Docker containers, we realized that none of the roles were setup for SSL let alone for the Identity server, even though the Identity container was included in the build.

Michael West’s Secure Docker Websites for Sitecore blog post https://michaellwest.blogspot.com/2020/01/secure-docker-websites-for-sitecore.html really helped. I still had to figure out the mechanics and here it is as I understand it.

  • Download Michael’s repo - https://github.com/michaellwest/docker-https
  • Modify the startup/createcert.ps1 file and set your wildcard domain, in my case lets say I have *.bemyfriend.local
  • Run the startup/createcert.ps1 in an elevated Terminal of your choice, this will generate the certs, pfx, txt files in the same folder
  • The script also installs the wildcard cert on your host machine. This prevents issues while rendering the sites or calling api with https.
  • Next modify your docker-compose file, in my case it was docker-compose.xc.sxa.yml. Look at the screenshots below:

CM Docker Compose

Identity Docker Compose

  • The entry point is the script which is run on the container when it starts, by default you would see entrypoint: powershell.exe -Command "& C:\\tools\\entrypoints\\sitecore-xc-engine\\Development.ps1 -WatchDirectoryParameters @{ Path = 'C:\\src'; Destination = 'C:\\inetpub\\wwwroot'; ExcludeFiles = @('Web.config'); }" but in our case we will use entrypoint: powershell.exe -NoLogo -NoProfile -File C:\\startup\\startup.ps1
  • We need to map our local folder .\startup which has the certs to the container’s C:\startup
  • We also bind port 443 to containers 44002 - the container port can be any unique port value if you choose
  • Set the network alias - cm.bemyfriend.local
  • Set the environment variable HOST_HEADER to cm.bemyfriend.local, this is the value which is picked up by the startup script in the entry point
[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [string]$EntryPointScriptPath = "C:\tools\entrypoints\iis\Development.ps1"
)
Write-Host "Running startup.ps1"
Import-Module WebAdministration
$website = "Default Web Site"
Write-Host "Checking if $($website) has any existing HTTPS bindings"
$hostHeaders = "${env:HOST_HEADER}".Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)
function Set-HttpBinding {
    param(
        [string]$SiteName,
        [string]$HostHeader
    )
    if ($null -eq (Get-WebBinding -Name $siteName | Where-Object { $_.BindingInformation -eq "*:80:$($hostHeader)" })) {
        Write-Host "Adding a new HTTP binding for $($siteName)"
        $binding = New-WebBinding -Name $siteName -Protocol http -IPAddress * -Port 80 -HostHeader $hostHeader
    } else {
        Write-Host "HTTP binding for $($siteName) already exists"
    }
    if ($null -eq (Get-WebBinding -Name $siteName | Where-Object { $_.BindingInformation -eq "*:443:$($hostHeader)" })) {
        Write-Host "Adding a new HTTPS binding for $($siteName)"
        $securePassword = (Get-Content -Path C:\startup\cert.password.txt) | ConvertTo-SecureString -AsPlainText -Force
        $cert = Import-PfxCertificate -Password $securePassword -CertStoreLocation Cert:\LocalMachine\root -FilePath C:\startup\cert.pfx   
        $thumbprint = $cert.Thumbprint
        $binding = New-WebBinding -Name $siteName -Protocol https -IPAddress * -Port 443 -HostHeader $hostHeader
        $binding = Get-WebBinding -Name $siteName -Protocol https
        $binding.AddSslCertificate($thumbprint, "root")
    } else {
        Write-Host "HTTPS binding for $($siteName) already exists"
    }
}
foreach($hostheader in $hostHeaders) {
    Set-HttpBinding -SiteName $website -HostHeader $hostheader
}

Write-Host "Running $($EntryPointScriptPath)"
& $EntryPointScriptPath

The script looked for the ${env:HOST_HEADER} value and binds the local iis instance. Since our cert is a wildcard *.bemyfriend.local, it lets us set host names for all our containers easily.

Identity Server

Now that we have the SSL certs out of the way, we can tackle the Identity server. As mentioned, the Identity Image is part of the repository but unfortunately the Identity configs are disabled by default.

Word of caution: I ran into some issues while running the Identity Server as ${REGISTRY}sitecore-xc-identity:${SITECORE_VERSION}-windowsservercore-${WINDOWSSERVERCORE_VERSION} (where version=1909) - I was getting No signing credential is configured. Can’t create JWT token error. Since we were not pushing any code to this container it did not make sense. I had to use ${REGISTRY}sitecore-xc-identity:${SITECORE_VERSION}-windowsservercore-${LEGACY_WINDOWSSERVERCORE_VERSION:-ltsc2019} for it to work without any issues (effectively setting it to 1809). Will update later if we manage to find the cause of this error.

If we take a look at the Dockerfile in the CM as shown below, the default scripts enable the following Config which in turn disables the Identity Server. Copy-Item -Path 'C:\\inetpub\\wwwroot\\App_Config\\Include\\Examples\\Sitecore.Owin.Authentication.IdentityServer.Disabler.config.example' -Destination 'C:\\inetpub\\wwwroot\\App_Config\\Include\\Sitecore.Owin.Authentication.IdentityServer.Disabler.config'

It also adds an IdentityServer.config which contain the overridden values for identityServerAuthority and FederatedAuthentication.IdentityServer.RequireHttpsMetadata.

The correct way is for us to fix the image and rebuild it. The easiest way for now is to reapply the patch configs and deploy those.

Place the IdentityServer.config in the App_Config\Include folder:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
    </sitecore>
</configuration>

Place the Sitecore.Owin.Authentication.IdentityServer.config in the App_Config\Include\Sitecore\Owin.Authentication.IdentityServer folder and modify only the identityServerAuthority setting to your Identity Server url:

<?xml version="1.0" encoding="utf-8"?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore role:require="Standalone or ContentDelivery or ContentManagement">
    <sc.variable name="identityServerAuthority" value="https://bemyfriendidentityserver.dev.local" />

    <settings>
      <!-- The URI of the IdentityServer provider. -->
      <setting name="FederatedAuthentication.IdentityServer.Authority" value="$(identityServerAuthority)" />
      <!-- The client identifier on the IdentityServer. -->
      <setting name="FederatedAuthentication.IdentityServer.ClientId" value="Sitecore" />

      <!-- Fill the FederatedAuthentication.IdentityServer.CallbackAuthority setting if you need another host to receive callbacks from IdentityServer. It is useful for reverse proxy configuration. -->
      <!--<setting name="FederatedAuthentication.IdentityServer.CallbackAuthority" value="http://proxy" />-->

      <!-- The client identifier for the Resource Owner Password flow on the IdentityServer. -->
      <setting name="FederatedAuthentication.IdentityServer.ResourceOwnerClientId" value="SitecorePassword" />

      <!-- Wheither HTTPS is required or not for the metadata address or authority -->
      <setting name="FederatedAuthentication.IdentityServer.RequireHttpsMetadata" value="true" />
    </settings>

    <services>
      <configurator type="Sitecore.Owin.Authentication.IdentityServer.ServicesConfigurator, Sitecore.Owin.Authentication.IdentityServer" />
    </services>

    <pipelines>
      <owin.identityProviders>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.IdentityProviders.ConfigureIdentityServer, Sitecore.Owin.Authentication.IdentityServer" resolve="true" id="SitecoreIdentityServer">
          <scopes hint="list">
            <scope name="openid">openid</scope>
            <scope name="sitecore.profile">sitecore.profile</scope>
          </scopes>
        </processor>
      </owin.identityProviders>
      <owin.initialize>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.InterceptLegacyShellLoginPage, Sitecore.Owin.Authentication.IdentityServer" patch:before="processor[@method='Authenticate']" resolve="true">
          <legacyShellLoginPage>/sitecore/login</legacyShellLoginPage>
        </processor>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.JwtBearerAuthentication, Sitecore.Owin.Authentication.IdentityServer" patch:before="processor[@method='Authenticate']" resolve="true">
          <identityProviderName>SitecoreIdentityServer</identityProviderName>
          <audiences hint="raw:AddAudience">
            <audience value="$(identityServerAuthority)/resources" />
          </audiences>
          <issuers hint="list">
            <issuer>$(identityServerAuthority)</issuer>
          </issuers>
        </processor>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.LogoutEndpoint, Sitecore.Owin.Authentication.IdentityServer" resolve="true" patch:before="processor[@method='Authenticate']" />
      </owin.initialize>
    </pipelines>

    <federatedAuthentication>
      <identityProvidersPerSites>
        <mapEntry name="sites with the core and unspecified database">
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='SitecoreIdentityServer']" id="SitecoreIdentityServer" />
          </identityProviders>
        </mapEntry>
        <!-- An example that maps a sub-provider of the Identity Server to the sites that are not mapped to the SitecoreIdentityServer. -->
        <!--
        <mapEntry name="all sites">
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='SitecoreIdentityServer/IdS4-AzureAd']" />
          </identityProviders>
        </mapEntry>
        -->
      </identityProvidersPerSites>

      <identityProviders>
        <identityProvider id="SitecoreIdentityServer" type="Sitecore.Owin.Authentication.IdentityServer.IdentityServerProvider, Sitecore.Owin.Authentication.IdentityServer" resolve="true">
          <caption>Go to login</caption>
          <domain>sitecore</domain>
          <enabled>true</enabled>
          <triggerExternalSignOut>true</triggerExternalSignOut>
          <transformations hint="list:AddTransformation">
            <transformation name="apply additional claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.ApplyAdditionalClaims, Sitecore.Owin.Authentication.IdentityServer" resolve="true" />
            <transformation name="name to long name" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="name" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
            <transformation name="role to long role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="role" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" />
              </targets>
              <keepSource>false</keepSource>
            </transformation>
            <transformation name="set ShadowUser" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.microsoft.com/identity/claims/identityprovider" value="local" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://www.sitecore.net/identity/claims/shadowuser" value="true" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
            <!-- owin.cookieAuthentication.signIn pipeline uses http://www.sitecore.net/identity/claims/cookieExp claim to override authentication cookie expiration.
                 'exp' claim value can be configured on Sitecore Identity server on the client configuration by IdentityTokenLifetimeInSeconds setting.
                 Note: Claim value is Unix time expressed as the number of seconds that have elapsed since 1970-01-01T00:00:00Z -->
            <transformation name="use exp claim for authentication cookie expiration" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="exp" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://www.sitecore.net/identity/claims/cookieExp" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
            <transformation name="remove local role claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.RemoveLocalRoles, Sitecore.Owin.Authentication.IdentityServer" />
            <transformation name="adjust NameIdentifier claim" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.AdjustNameIdentifierClaim, Sitecore.Owin.Authentication.IdentityServer" resolve="true" />
          </transformations>
        </identityProvider>
        <!-- An example of how to add an identity provider as a sub-provider of the Identity Server.
             The 'name' property must be in the following format: SitecoreIdentityServer/[AuthenticationScheme], where the 'AuthenticationScheme' equals the
             authentication scheme of an external identity provider that is configured on the Identity Server.

             Notes:
               1. The 'TriggerExternalSignOut' and 'Transformations' properties are inherited from the the Identity Server provider node and can not be overridden.
               2. To use a sub-provider, the 'Enabled' property of the Identity Server provider must be set to 'Enabled'. -->
        <!--
        <identityProvider id="SitecoreIdentityServer/IdS4-AzureAd" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">$(id)</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>Log in with Sitecore Identity: Azure AD</caption>
          <icon>/sitecore/shell/themes/standard/Images/24x24/msazure.png</icon>
          <domain>sitecore</domain>
        </identityProvider>
        -->
      </identityProviders>

      <propertyInitializer>
        <maps>
          <map name="set IsAdministrator" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <source name="http://www.sitecore.net/identity/claims/isAdmin" value="true" />
              <target name="IsAdministrator" value="true" />
            </data>
          </map>
        </maps>
      </propertyInitializer>

    </federatedAuthentication>

    <sites>
      <site name="shell" set:loginPage="$(loginPath)shell/SitecoreIdentityServer" />
      <site name="admin" set:loginPage="$(loginPath)admin/SitecoreIdentityServer" />
    </sites>
  </sitecore>
</configuration>

Once the above is done, file publish your solution to the mapped .\data\cm\wwwroot:C:\src folder, followed by loading your https://cm.bemyfriend.local in an incognito Chrome browser.

Credit where its due

  • Kamruz Jaman - Thanks for all the help and guidance. I wish I was as creative as he is with Photoshop. Images always courtesy of his creations.
  • Michael West - Thanks for the SSL blog post

If you have any questions, please get in touch with me. https://twitter.com/akshaysura13 on twitter or on Slack.