Friday, February 15, 2013

Optimizing IIS for Performance & Security

My college uses Microsoft's IIS 7 for its servers instead of the more common Apache. That's fine; IIS is probably a good server. I don't know, I'm not qualified to say which is better. But one thing's for sure: Apache is a easier to use & learn simply because of the availability of documentation. If you're a full stack web person starting a new project, please use something with community support & documentation. Apache plays nice with Drupal, there's tons of security & performance tweaks documented online, & it has some great add-ons for any situation.

But hey, I'm stuck with IIS. This post is mostly a note-to-self on how to optimize IIS. I'm not at all a server configuration expert, so please don't take it as gospel. Most especially, if I'm flat-out wrong about something, I'd like to hear about it.

For the tl;dr & the resulting file, see my web.config github repo.

Caching

The hardest part is caching correctly. The goal is to use far-future expires headers, similar to Cache-Control: max-age=9000000. There are many different means of caching in HTTP but far-future expires is both simple (the server just says "hey, you can hang onto this content for X seconds") & effective. Some other caching methods end up sending "conditional get" requests, essentially saying "hey, server, I have version 3.2 of this file, is that current?" & the server sends a response back saying either "yup, carry on" or "nope, here's the current version." That is slightly less error-prone, because you can update a file on the server & it'll still make its way to clients that have cached the content, but that extra HTTP request adds up quickly. To update files with max-age or other far-future type caching schemes, I use filename-based versioning, essentially bumping a version number like "style.1.css" to "style.2.css" every time I change a file. Because remembering to change filenames is tedious, I either have a CMS (Drupal's built-in caching) or a build script (Yeoman) handle it for me.

In IIS 7, unfortunately, it looks like you can either set static content caching on or off with little in between (Apache lets you specify expires time by MIME type). If there's a particular static MIME type that you don't want to get cached, too bad. That's problematic for at least two types: text/html & text/cache-manifest. These are both static, text types but the files need to be able to change without changing their name. If you altered your HTML file's name every time it changed, you'd constantly break incoming links. The appcache can't change because it causes this weird loop wherein clients that have previously visited the site & primed their cache can never get an updated version because they always looks in the wrong place; Jake Archibald covers this brilliantly in Appcache Douchebag.

So to get around this conundrum, I use two layers of web.config files: in the site's root where HTML, server-side scripts, & the cache manifest reside I use a config with no caching whatsoever, that's <clientCache cacheControlMode="DisableCache" />. Then, in any subdirectory where static content (images, CSS, JavaScript, fonts, etc.) might reside, I override that setting with an aggressive, far-future expires header.

Finally, I remove ETags with a two-part rule. The HTML5 Boilerplate server configs botch this horribly, ruining the X-UA-Compatible header in the process, but some searching around StackOverflow found me the right combination of rules to remove ETags per performance best practice (see Steve Souders' book).

GZIP

I just copied this bit from the HTML5 Boilerplate Server Configs & made sure it worked with YSlow & other external tests. It's super important to GZIP content, arguably the biggest performance win you can get, & yet that's not the default in IIS 7.

Security

I'm not an expert at hardening servers but it makes sense to eliminate headers that unnecessarily expose server information without any added benefit. I blank the X-AspNet-Version, X-Powered-By, & Server headers. Another IIS quirk is that you can't simply remove the Server header, all you can do is set its value to be an empty string which is at least enough to protect against the version number being exposed.

Rendering Engines

Since the X-UA-Compatible meta tag doesn't really work, I send it as an HTTP header. This forces IE to use Chrome Frame if it's available or the latest rendering engine (e.g. no IE 8 using the IE 7 engine) if not.

No comments:

Post a Comment