Protecting the Laptop Herd
Dec 21, 2009 at 4:26PM
Craig Brozefsky Greetings from the Playbook Development team, deep in the Ruby Mines of Matasano. I’m Craig Brozefsky, lead developer, and senior curmudgeon. Back when OSX was still called NeXTStep, but after the fall of suck.com, I was an ObjC hacker. I have fond memories of the time, despite the daily SpaXewars beatings at the hands of my co-developer, Jesse. So, when Tom informed the team we would be adding support for OSX Host Firewalls and needed to whip up a native OSX application, I saw my chance to pretend I was 10 years younger.
Playbook as Border Collie
Back then, developers used workstations — I had a Sun E1 and a NeXTCube. They heated your office, irradiated your head with giant CRT tubes, and had keyboards so big you could take naps under them!
Now it is customary to issue employees a laptop so they can work from bed. This is a great advance. They are small, make your thighs sweaty, ruin your eyesight, and torque your wrists — in bed. A single modern laptop can run the half-dozen virtual machines needed for modern web development using portable standards like CSS and HTML.
This proliferation of machines which move in and out of the office present a challenge to the company’s Hall Monitors, or network security ops as they like to call themselves. If Bob uses the company laptop at home, in bed, then we can’t enforce company web surfing policy at all times. That machine could have anything on it when it plugs back into the office network!
Luckily, the laptop has its own firewall built in, we just need a way for the Hall Monitors to manage the rules. Provided such a mechanism, they could key protect laptops with blacklists of drive-by malware sites, limit incoming connections to known services, limit outgoing connections to specific services. Then the next time Bob is getting cream cheese and sesame seeds all over the keyboard while checking email at the cafe, his host firewall is blocking the SMB attack launched by the unemployed financial analyst turned greasy-haired hacker in the corner. This is the stuff Hall Monitor dreams are made of.
Chaos Breeds Requirements Docs
An employee’s laptop is somewhat sacred, so we didn’t want to have
creds for each laptop in some central database. Eric threatened
mutiny at such a prospect anyways, perhaps out of fear we would use
the creds to scan his laptop and find the pirated set of Little House
novels in his ~/.snekrit directory.
Since I only arrive in the office on time on prime Julian dates or when Tom is buying dinner, we could not realiably schedule a push from our dogfood Playbook instance to each laptop. Inspired by NCSA Mosaic and the awesome power of HTTP technology, We opted to write a client that would run under each user’s account and pull rules down from a Playbook server.
As Cory mentioned, Matasano has been an Apple shop since 1925, so it was natural that we added support for OSX’s ipfw first. We could torture our own employees with the early releases, all under the guise of protecting them.
Keep Talking
We worked up a list of requirements for the client and server communications.
- Client and Server must be authenticated
- Comms must be encrypted
- Laptops should not require a Playbook login
- Enrolling a new laptop should be easy
- Revoking a laptop should be easy
We wanted the authentication to be automated, not dependent upon user password choice, and verifiable in both directions. The obvious solution here is SSL with certificate-based authentication on both sides. Since Playbook is accessed via an Apache instance, using Phusion Passenger, we can rely on Apache to verify the client certificate.
To manage the client certificates we built our own CA based on the QuickCert Gem. We create a key and certificate for each client.
We need to tell Apache to require a valid client certificate for requests to the pull controller, and to only accept one signed by our Playbook CA. Also, in order to allow us to manage Client Certificates without dealing with revocations, we pass along the certificate in the request headers.
<Location /pull>
SSLVerifyClient require
SSLOptions +StrictRequire +StdEnvVars +ExportCertData
RequestHeader set X-Client-Cert %{SSL_CLIENT_CERT}e
SSLVerifyDepth 1
</Location>
We don’t want to require a valid client certificate when the laptop is enrolling, aka downloading, the native client.
<Location /pull/enroll>
SSLVerifyClient none
</Location>
Lastly, we tell Apache were the CA Certificate for our client pull subsystem is located.
<IfDefine production>
SSLCACertificateFile "../../data/client_pull_ca_production/cacert.pem"
</IfDefine>
Our controller needs to make sure the client certificate supplied to
us is the same one we have on file for the host in question. The
HTTP_X_CLIENT_CERT entry in the envrionment table contains the
certificate, so we normalize it and generate an SHA1 checksum. That
is compared with a normalized SHA1 checksum of our certificate on
file.
Here is the relevant method:
def check_cert(fw)
cert = request.cgi.env_table['HTTP_X_CLIENT_CERT']
if cert.blank?
raise "No client certificate provided"
end
if fw.client_pull_certificate.blank?
raise "Firewall does not have a client pull certificate"
end
cc = cert.gsub(/\s/,"")
fc = fw.client_pull_certificate.gsub(/\s/,"")
if SHA1::sha1(cc).to_s != SHA1::sha1(fc).to_s
raise "Client pull certificate does not match."
end
end
Ragel Rock
Before I came to Matasano, I was a Java hacker at the Center for Connected Learning an Computer-Based Modeling, where my job was working on NetLogo. This involved work on lexers, parsers, interpreters, compilers, and other aspects of programming language design. I liked that alot. In Playbook, we’re interested in the lexing and parsing, and some analysis of a range of firewall languages.
Writing lexers and parsers is, well, boring and rewarding at the same time. It’s a tedious cataloging of the “parts of speech” and grammar rules of a language. If you have good documentation for your target language, it is mind-numbing. With poor or no documentation it is maddening. Pray to whatever gods or old ones you believe in that the language is regular and not just “defined by implementation”. Despite this, it is rewarding when done, because you have a really useful tool for analyzing the target language.
This is precisely the kind of task where a good tool for building your lexers and parsers can save you thousands of dollars in time and prescription sedatives.
Adding IPFW rules support to Playbook took an afternoon thank to Ragel and our own lexer toolkit built on top of it, Ralex. Ragel lets you define reusable finite state-machines — it’s like regular expressions turned inside out with hooks all over. Or, as the projct site says:
Ragel compiles executable finite state machines from regular languages. Ragel targets C, C++, Objective-C, D, Java and Ruby. Ragel state machines can not only recognize byte sequences as regular expression machines do, but can also execute code at arbitrary points in the recognition of a regular language. Code embedding is done using inline operators that do not disrupt the regular language syntax.
Ralex is our own class which includes a set of Ragel state-machines that match tokens common to firewall rule languages, and then some book-keeping code that takes a string and a set of keywords, and gives you a token stream usable as RACC input.
When we want to write a rule tagger and parser for a new rule language, we start with the base Ralex clss:
class RalexIpfw < Ralex
%%{
machine ralex_ipfw;
include ralex_base "ralex_base.rl";
Next then we define any new lexemes needed for this specific rule language. Usualy this is limited to idiosyncratic representations of percentages/bandwidth limits or port/IP ranges.
hexnumber = '0x' @collect_token hexdigit+
@collect_token
>{ start_token(:HEXNUMBER) }
;
probability = '.' >collect_token digit+
@collect_token
>{ start_token(:NUMBER) }
;
ipwithmask = ipaddr ":" @collect_token ipaddr
%{ start_token(:IPWITHMASK)}
;
macwithmask = macaddr "/" @collect_token digit+ @collect_token
>{ start_token(:MACWITHMASK)}
;
portset = ( alnum | '\-' | digit )+ '-' @collect_token ( alnum | '\-' | digit )+
@collect_token
>{ start_token(:PORTSET) }
;
slashcomment = '/' '/' >collect_token >getcomment ;
bandwidth = digit+ @collect_token ('K' | 'M') @collect_token ('bit' | 'Byte') @collect_token '/s' @collect_token
>{ start_token(:BANDWIDTH) }
;
Lastly we identify which state-machines we want to use when parsing a rule:
token = ( id
| ipaddr
| ipnet
| ipwithmask
| macaddr
| macwithmask
| portset
| bandwidth
| hexnumber
| number
| percentage
| bytecount
| ralex_specials
| slashcomment
| hashcomment
| probability
| quote ) %{ finish_token; };
After we have our Ralex class for the new firewall language, we then build two RACC parsers on top of that. One tags significant rule elements, like addresses and ports, which drives our rule indexing engine. The second one does full rule parsing and performs syntax checks and generates a ASTish representation of rules for use in or analysis tools.
Since we have a bunch of these written for other rule languages already, it’s pretty easy to put a new one together. The most time consuming, mind-numbing bit is encoding all the keywords in the rule languages, which we currently have to triple code cause I’ve been to lazy to refactor.
This whole process is driven by a bunch of test macros we have for taking examples rules and deriving tests for the tokenizer, tagger, and parser classes.
it "should handle ip address representations" do
token_test("from 192.2.2.2 to any",
[[:FROM, "from"], [:IPADDR, "192.2.2.2"],
[:TO, "to"], [:ANY, "any"]])
# masklen
token_test("from 192.168.1.0/25 to me",
[[:FROM, "from"], [:IPADDR, "192.168.1.0/25"],
[:TO, "to"], [:ME, "me"]])
....
it "should tag addresses and ports " do
parse_test("add deny ip from 123.45.67.0/24 to my.host.org",
["add", "deny", "ip", "from", IPAddr.new("123.45.67.0/255.255.255.0"),
"to", Hostname.new("my.host.org")])
parse_test("add deny ip from 123.45.67.0/24 to my.host.org 80",
["add", "deny", "ip", "from", IPAddr.new("123.45.67.0/255.255.255.0"),
"to", Hostname.new("my.host.org"), PortNumber.new(80)])
At the end of the afternoon I had my tokenizer, and a tagger. We don’t have a full parser yet, so there are no syntax checks for IPFW in the 3.0 release. After defining a new Firewall and Line (as in rule line) class for IPFW, I had working models, and was ready to start on the controller and the OSX client.
Back to the salt mines, for now.
That concludes the server portion of our program, in the next episode I’ll gush a bit about ObjC, and reveal the inner secrets of our OSX client.

