I have a bit of a quandary that’s got me effectively stuck on a task at my day job. Thus far, Google and every other resource I’ve searched have been little help. In the unlikely event somebody out there that reads this blog (or at least gets the update notices via RSS, Twitter, or the other various feeds) can help me, I’m going throw this out and hope it garners some feedback.
I’ll try to keep this as short as possible. Our production Web site, built in ASP.NET and C# and running in IIS on Windows Server 2003, recently added authentication via client certificates stored on users’ smart cards. We allow users to attach their smart card certificates to their existing account, then authenticate them by verifying their certificate, looking up the user account by that certificate’s fingerprint, and loading their profile. These certificates are signed by a trusted third-party certificate authority (CA) owned by the client and every morning we download the latest certificate revocation lists (CRLs) so we can reject certificates as they are revoked by the CA. My download process is working fine and dandy, so that’s not the problem; neither is the actual import process, as I know the command line options for Microsoft’s certutil command that will import the CRLs.
My problem stems from removing the old CRLs, which so far I haven’t been able to accomplish without going into the Microsoft Management Console and clicking through the GUI. We’ve had problems with the size of the certificate store, as the CRLs tend to be very large and we have to remove the old ones before the new ones can be imported. I’ve tried the few suggestions I’ve found online that haven’t seemed to work, such as a command-line switch for certutil that’s supposed to overwrite the old CRL with the new one (it just imports the new one and leaves the old one in place). We want to automate this process into a scheduled task, so it can run early in the morning when our users aren’t on the system and without human intervention.
Here are the tools available to me:
certutil (part of Microsoft’s Certificate Services package);I’ll tell you, I’m pretty frustrated and exhausted by this task. It’s not that I can’t do the research and figure it out for myself; I have done the research, and everything I’ve read applies to certificates and not CRLs, and they’re not exactly a direct swap in usage. I’d prefer not to provide much more detail than this for security reasons.
For the time being, I’ve been manually removing the old CRLs through MMC and then running a batch script to do the import every morning as my first task. That’s working fine for now, when I’m in the office every morning, but I’ll be taking some vacation time soon that will start to cause problems. I swear, if this was OpenSSL and Apache on Linux, I’d have this solved in a heartbeat (or at least an afternoon). If you have any suggestions, please feel to post a comment or shoot me a direct e-mail at the usual address.
I mentioned last week that I was working on a neat Apache mod_rewrite trick for locking down access to certain administration pages, but that I wasn’t having much success with it. Well, it seems to be working now and, as promised, I wanted to share it with anyone who might be interested. Fair warning to non-technical readers: extreme geekery lies ahead.
First and foremost, I can’t claim full credit for this idea. It borrows some from Steve Gibson’s roaming authentication scheme outlined in episode #113 of the Security Now! podcast. In that show (and subsequently continued in episode #115), Gibson outlines his method of allowing his employees to access secure portions of his site while traveling. The method described here is not quite as secure as his, as I’m forcing things to happen at the Web server software layer as opposed to the application layer and thus don’t have the same fine granularity of control he has. However, it uses many of the same ideas.
It’s relatively easy with mod_rewrite to protect certain resources of a site by restricting access to certain IP addresses. Consider the following:
RewriteCond %{REQUEST_URI} ^/store/admin/.*
RewriteCond %{REMOTE_ADDR} !^192\.168\.13\.
RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1$
RewriteRule ^/store/admin/.* /store/ [R,L]
This rule set essentially says: (1) if the requested URL starts with the string “/store/admin/” and (2) the IP address of the requesting client does not begins with “192.168.13.” or (3) is not exactly “127.0.0.1″ then (4) redirect all requests for URLs starting with “/store/admin/” to the root URL of the store, “/store/”. Essentially, we’re only allowing access to what is apparently the administrative portions of an online store to a very limited number of IP addresses, one of which is fully qualified (the “loop-back” address of 127.0.0.1) and the rest belonging to a range (192.168.13.0 through 192.168.13.255). Anyone outside these IPs will be transparently redirected to the front page of the store. (Redirecting is much friendlier than outright forbidding access.) All of this takes place in Apache itself, before we even get to the application and any potential security flaws it might have. There are no worries about hacking the store software itself to deny access. Of course, we can list any number of REMOTE_ADDR entries that we wish; each condition is a regular expression (which are negated here by the “bang” at the front) so we can filter on any octet we want and can easily specify real, outside IPs rather than private ones. For example, for this site I limit access to my various admin sections to the IP of my cable modem and our outside IP at work.
However, what happens when you are required to go on a trip and need to access the administrative parts of the site while on the go? Obviously, you can’t add the hotel’s outside IP to this rule set in advance (imagine asking the front desk for that information), and you probably won’t be able to add it easily once you get there. Sure, WordPress and the store front software have login security on their various admin interfaces, but we’re trying to protect those from hackers, right? Aside from reopening them to the entire Internet before the trip and closing them again once we get back, there aren’t very many options. How then can we identify approved “roaming” users and/or machines so they can access the admin sites without being inside a hard-coded list of IPs?
Gibson’s answer was to optionally set a secure cookie in the user’s browser if they access the admin site within one of the approved IPs first. Being within an approved IP, they aren’t restricted by the access rule and they are allowed to reach the login prompt. During login, they are prompted on whether or not they want to enable roaming access on this particular machine. If they agree, a secure cookie is set in the browser and set to expire at some date in the future. Later, when the user attempts to access the admin site outside of the approved IP list, the site checks to see if the cookie has been set. If present, the user is allowed to log in, just as if they were within one of the approved IPs. The cookie acts as a kind of two-factor authentication: the first factor being “something you know”, the user name and password, and the second being “something you have”, the cookie. Since the cookie is set in secure mode (HTTPS), it will only be sent back to the site over a secure connection. And since (well behaved) browsers only allow a site to read the cookies it has itself set, no other site should be able to read it.
This is all well and good… if you have access to the source of the application you’re trying to secure and you’re willing to hack it. Gibson wrote his own store front, so this was relatively easy for him to integrate. But I want to secure WordPress, a third-party store app, and a few random subdirectories that are pretty much statically built HTML. As much as I like running Open Source software, I usually prefer not to muck around with things if I can help it, lest I screw something up. Thus, I don’t particularly want to hack WP and the store to add this extra layer of functionality. Fortunately, though, mod_rewrite gives us a mechanism through which we can accomplish basically the same thing without modifying the underlying application. In theory, since all this occurs before we even reach the application, one could argue it may even be more secure than the application’s authentication mechanisms themselves.
You can actually set browser cookies via mod_rewrite rules. Consider what happens if we insert the following before the rules we defined above:
RewriteCond %{REMOTE_ADDR} ^192\.168\.13\.
RewriteCond %{HTTP_COOKIE} "!(.+; )*admincookie=uniqueval(; .+)*"
RewriteRule .* - [CO=admincookie:uniqueval:.domainname.tld:43200:/store/]
This rule set essentially says: (1) if the remote IP starts with “192.168.13.” and (2) there isn’t a cookie already set by the name “admincookie” then (3) set a cookie named “admincookie” with the value “uniqueval” for the domain “.domainname.tld” (assuming that’s our real domain name) for a period of 30 days (60 minutes x 24 hours x 30 days = 43,200 minutes) restricted to the path “/store/” and its subdirectories. Now let’s modify the rule set from before:
RewriteCond %{REQUEST_URI} ^/store/admin/.*
RewriteCond %{REMOTE_ADDR} !^192\.168\.13\.
RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1$
RewriteCond %{HTTP_COOKIE} "!(.+; )*admincookie=uniqueval(; .+)*"
RewriteRule ^/store/admin/.* /store/ [R,L]
Note that we’ve added a new condition. In addition to checking for the approved IP list, we also check to see if the “admincookie” has been set and that its value is what we expect (“uniqueval”). Note the parenthetical parts at the beginning and end of the cookie regex; these should make sure we match the unique cookie name/value pair, regardless of how many cookies are present. (Also note the quotes around this regex; since whitespace delimits the parts of the rewrite statements, the quotes are required to include the spaces after the semicolons in the regex. Without the quotes, the regex produces a “bad flag delimiters” error when Apache parses the configuration file.) Since each approved item’s entry is negated, the rule is only applied if none of them match. So now we should be able to get into the site remotely if and only if we’re inside an approved IP or we have the secret cookie, which we know is only set if we’ve been in one of the approved IPs first. Instant roaming authentication!
To summarize, the primary advantages to this scheme are:
There are, of course, a few caveats:
mod_rewrite to force certain URLs to always use SSL (assuming you have a secure certificate), thereby securing the connection first. All WP admin functions, the GPF Store, and my other secured admin locales here on this site are all secured via SSL, so that helps in keeping my site secure by eliminating sniffing. (Of course, if you go this route, don’t forget to copy any necessary rules from the main Apache configuration file to the SSL config file, as the secure site will be treated as a different virtual host with its own set of rewriting rules. This little hiccup is what was keeping me from publishing this for quite a while.)mod_rewrite does not have the facility to specify secure mode in a cookie set by a rewrite rule. Thus, the above cookie is not secure and will be sent with each request in or below the specified path, encrypted or not. The cookie is then theoretically susceptible to sniffing attacks. Setting a secure mode cookie is easy enough to do in application code, but not apparently so in mod_rewrite.mod_rewrite. (Remember, all this is occurring in Apache before we even reach application code.) Right now, %{HTTP_COOKIE} variable gets all the cookies for a given site/path as one big string, with each name/value pair delimited by a semi-colon and a space (“; “) and the name and value are glued together with an equal sign. I’m looking into a better regex to match this more precisely and I’ll update this post if I find one.I welcome any feedback on how to improve this, especially if anyone knows how to get around the secure and unique cookie caveats.
Appendium: I should also point out that this scheme should be equally usable if you place the code in your master Apache configuration file (usually something like /etc/httpd/conf/httpd.conf on UNIX clones) or in per-directory .htaccess files. I usually prefer to put such rules in the master config file, mostly because it’s more secure (outside of the document root) and only gets parsed and loaded once while .htaccess files are read and parsed each time there’s a request in that directory (or any of its subdirectories). However, that only works if you have access to the master config, which most shared hosting services don’t provide. Of course, such rules placed in an .htaccess file will only apply to that directory and its subdirectories, so you’d have to tweak the rules (such as file paths and the cookie path) as necessary.
Update 11/20/2007: Updated cookie regex to better match the exactly name/value pair; added notes about rotating cookie values.
Update 11/30/2007: Put cookie regex in quotes to correct avoid “bad flag delimiters” parsing errors; added advantage summary to better showcase the advantages of the scheme; updated my cookie value scheme; added highly-random subdirectory alias to avoid unintentional cookie-ing