Azure DevSecOps: Pipelines & Web Config

Scott Anderson
8 min readFeb 17, 2020

--

Content Security Policies & The Occasional Silent Failure

The Preface

Forewarned, this one got away from me a little! It covers the usage of ‘Best Practice’ Web-Server Security Headers, specifically for Microsoft’s Azure Platform and AzureDevOps (Formerly Visual Studio Team Services). Over my Time at Head Full of Heart I’ve also come across a few little ‘gotchas’ that come along with these headers and some tools that make life easier.

The Web.Config Best Practice

The group at OWASP have a nice project called the “Secure Headers Project”. It lists and lays out all the headers you should probably be sending from your web-server of choice. In the case of Azure WebApps, it’s Microsoft IIS. The headers themselves applicable across all the major Web Servers (Nginx, Apache, IIS, Kestrel, etc…), with their applicable syntax applied.

If that sounds good and you just want the code, there is an example of the “Bare Starting Minimum” here, and the “Gold Standard” here. There is also the jokingly titled “Draconian” here. Just keep in mind that your security needs will certainly be different, and these are just some starting points!

While I won’t list every header you can send (the full list can be found at https://www.owasp.org/index.php/OWASP_Secure_Headers_Project) The absolute bare minimum is used at Head Full of Heart are the following:

  • HSTS
  • X-Frame-Options
  • X-XSS-Protection
  • Content Security Policy (CSP)
  • X-Content-Type

As these are very much settings-per-web-server and settings-per-web-application, there is absolutely nothing stopping you having boilerplate, secure-by-default configurations that need only a little tweaking (usually, it’s all in the CSP Header). For me, it starts out looking something like this:

<httpProtocol>
<customHeaders>
<remove name=”X-Powered-By” />
<add name=”Strict-Transport-Security” value=”max-age=86400"/>
<add name=”X-Frame-Options” value=”SAMEORIGIN” />
<add name=”X-XSS-Protection” value=”1; mode=block” />
<add name=”X-Content-Type-Options” value=”nosniff” />
<add name=”Content-Security-Policy” value=”default-src ‘self’/>
</customHeaders>
</httpProtocol>

CSP Headers and Interesting Failures

The CSP is funny thing, and can cause all sorts of interesting failures, and the best way to fix it is to break things. It can also break dependencies in a silent manner.

We had an interesting one where a form service was getting blocked, ninja-style. None of the usual console errors, non on-screen errors, no breakpoints showing issues, there was (more than) a little bit of tail chasing. Application Insights caught the error for us: CSP was blocking a NodeJS module that utilized an inline-eval that was a part of our form service.

Cue much relief, and a quick patch later the forms worked again — with equal parts ‘That’s good, the CSP works as it should’ and ‘How do we deal with this potential issue’. Enough of our collective misery — back to fun stuff!

For Azure, the best way to test a CSP is via the ‘App Service Editor’, where you can make like changes to the web.config file (actually, any file on the service) and test your CSP live before committing it to your source control, all in a VS-Code-esq editor in browser.

My advice would do not be tempted to write the CSP, commit to source, deploy via pipeline and then load up your app; you will be left looking at your broken app, wondering why your fonts are gone, images wont load or third-party-integrations are down.

This is rather expensive in terms of time spent tracking down all the required URLs to add to your CSP. Pick the way that takes you least amount of time!

A Reminder: web.config files are volatile! A badly formatted command, unclosed xml tag or any other syntax-based errors will, on Azure, throw your app into complete shutdown, with a 500 Internal Server Error. Until you fix the error, and then it will be back online within seconds.

The ‘Gold Standard’

I briefly mentioned the Feature Policy, but we also have a few other possible headers that can tighten the screws a little bit more and keep to that ‘Gold Standard’ of doing all the security things right.

The last two are the ‘Feature-Policy’ and ‘Referrer Policy’ headers.

The Feature Policy is fairly new (by header standards), it only came in the latter half of 2018 and is more focused on keeping mobile device features away from malicious attackers. It doesn’t have full support everywhere (yet), but all the major browser has it implemented to varying degrees.

It functions in a very similar way to the Content Security Policy Headers. It has restrictions on things like vibration, notifications and microphone, just to name a few. The spec is over here, and you can see it in both the ‘Gold Standard’ and ‘Draconian’ examples.

Referrer Policy is older, circa 2017, and is specifically for controlling when and how the Referrer header is sent in requests. Sometimes, you might want to prevent specific routes or other from HTTPS to HTTP that can leak via the Referrer Header.

Pulling all of these things together creates a web.config file that can be called a ‘Gold Standard Starting Point’ and is what Head Full of Heart and Taptu prefer to start with, then lock down further as we need.

That Little Extra Dragon

Draconian, get it? Anyways, you can restrict nearly everything with your web.config, and some of it can rightfully be classed as security theater, the dog-and-pony-show of security. When we get down to restricting individual HTTP Verbs from interacting with our WebApp then we should have a very good reason. For this case, its due to the compiled Angular app not needing to receive anything but GET. And with a few lines, that is all it can do.

It should be clear, but this only affects incoming traffic. The app itself can still POST/PATCH/PUT/Etc. to API end-points to its hearts content.

<security>
<requestFiltering removeServerHeader=”true”>
<verbs>
<add verb=”PUT” allowed=”false” />
<add verb=”HEAD” allowed=”false” />
<add verb=”POST” allowed=”false” />
<add verb=”DELETE” allowed=”false” />
<add verb=”CONNECT” allowed=”false” />
<add verb=”TRACE” allowed=”false” />
<add verb=”PATCH” allowed=”false” />
</verbs>
</requestFiltering>
</security>

You can go even further, and start looking at the other headers and options for re-write rules and blocks, but there is a very good test to apply before you go too far: Ask yourself — “Do I need that?”.

A good test we use here at Head Full of Heart is the Security Headers Tester site. It’s an excellent grading tool and helps stop the silly errors that get people in trouble. While not the absolute be-all-end-all, its over at https://securityheaders.com.

Stopping the Little Tidbits

As a security person, I very much enjoy talkative services, of any type. It makes figuring out plans of attack much easier when it coughs up name, version and framework without me having to lift a finger beyond ‘F12’. Preventing these little hints at every opportunity make it just that little bit harder for nefarious actors to gather intelligence, adding to the defense-in-depth.

The snippets beforehand actually had two such restrictions. As with all things, they are scattered around the configuration, never in the one place.

<requestFiltering removeServerHeader=”true”>
<remove name=”X-Powered-By” />

So, what does that do? Well, it does what it says on the tin!

One removes the X-Powered-By Header, where you usually get a nice marker saying ASP.NET or perhaps Servlet/3.0 or other information of what is running behind-the-scenes.

Another Note: Removing the X-Powered-By header only works if it is the web-server injecting that Header. If you have (as an example) a .NET Core API that’s running in a Web App, then that line will do nothing. Any type of middleware will also tamper with this.

The other removes the Server Header, which is IIS, Kestrel, Apache, Nginx or whatever your web-server is, and sometimes, depending on the config, you also get the version number in the server header.

Why bother?

Every little bit of information helps when gathering information. If I can tell that the webpage was served from an embedded HTTP server, I can take a good guess as to its limitations.

From there, if I have the back-end technology I can narrow my guesses even further. Keeping the bad guys guessing means it takes them longer and they are (hopefully) more likely to give up.

But where is the Pipeline?

There’s been much waffle of web.config and assorted security measures and headers and standards and all the fluff around it, but how to get this into a CI/CD Pipeline?

For Azure DevOps Pipelines this can take many approaches; variable substitutions/xml transformations, deployment tasks, build tasks… there are so many ways to get that important config file up and through the pipeline.

Personally, I like to apply Occam’s Razor, that the simplest also tends to be the best.

No matter the approach, you will need some sort of “web.config” file in your source control, and as there isn’t anything sensitive in the file (for security headers), and there isn’t any need to run it though say, Azure Key Vault.

For Angular CLI Projects, this can be done with no changes to an existing pipeline and all. It’s a single line in your angular.json file. By adding a reference to your web.config (where ever it may be in your repository) to “assets” under the “build” the Angular Compiler will grab it and include it into build.

It’s that simple, that is, if a single web-config suits your deployment. Chances are, this just isn’t going to cut it, and for Head Full of Heart, it certainly doesn’t — deploying the same web.config and headers to Development, Quality Assurance, User Acceptance Testing and Production is just asking for trouble, and broken environments all day long. No thank you!

A side note: It is entirely possible to abstract away many of the URLs using the ‘*’ wildcard, thus a single web.config usable all the way through then entire pipeline and their associated environments. I prefer not to, as they can get you into trouble, but as always, it depends upon your security needs.

Enter Config Transforms

Configuration transforms are all sort of awesome, and they neatly solve the problem of having to commit a full web.config to your repo; for AzureDevOps Pipelines, they come in two flavors — XML and JSON.

I’ll be focusing on the XML variant for this one, but the JSON transforms follow the same base principles. I won’t go over all of the possible options, as XML transforms have been around for quite some time.

For Azure, where I spend most of my time, this can be done by crafting the web.config a little differently and the introduction of a new files per environment — web.debug.config. This file tells the xml transform how to override/update/transform/remove the xml in the web.config file, allowing us to specify variables needed.

In short, your ‘web.config’ file becomes a template of what you want filled. The transformations for an environment reside in a new file ‘web.debug.config’. Simply make sure your WebApp Deploy Task has the XML Transformation box ticked, and ‘Copy if Newer’ is specified for the config file (or it might not overwrite the file if you update the transformations). It can get a little complicated, but the base of it is pretty easy.

There is also a pretty handy tool to play around with how it all works here. The Microsoft page for file and variable substitutions is well done and is sitting here.

That’s It!

Thanks for sticking with such a long article, and hopefully it was a little helpful! Want to know more? Throw a question at me on Twitter at @TeamHFoH, @TaptuIT or @TheStudyScott.

--

--

Scott Anderson

PhD Student and Graduate Cybersecurity Specialist at Head Full of Heart.