Getting Things Done with CouchDB, part 2: Security |
2012-09-15
|
(... wherein I disappoint the readers who were planning to drop my database)
So, I assume you now know what mushin is.
The goal for my nine/eleven hack day was simply to add authentication everywhere to mushin, making it as user-friendly as possible, and as secure as couchdb was going to let me.
Now, CouchDB's security story has always been a little confusing to me. It's gotten better over the years, though, so it was time to revisit it and see how far we could get.
By default, CouchDB listens only on localhost, uses plaintext HTTP, and is in Admin Party mode. Which means, anyone is an admin, and anyone who can do a request on localhost can create and delete databases or documents. This is really useful for playing around with CouchDB, learning its REST API using curl. So easy in fact that it's hard to go away from that simplicity (I would not be surprised to find that there are companies out there running couchdb on localhost and unprotected).
Authorization
What can users do ? What type of users are there ?
In a nutshell, couchdb has three levels of permissions:
- server admin: can do anything; create, delete databases, replicate, ... Is server wide. Think of it as root for couchdb.
- database admin: can do anything to a database; including changing design documents
- database reader: can read documents from the database, and (confusingly) write normal documents, but not design documents
CouchDB: The Definitive Guide sadly only mentions the server admin and admin party, and is not as definitive as its title suggests. A slightly better reference is (although I still haven't cleared up what roles are to be used for, beside the internal _admin role).
By far the clearest explanation of security-related concepts in CouchDB is in Jan Lernhardt's CouchBase blog post.
I'll come back to how these different objects/permissions get configured in a later post.
Authentication
How do you tell CouchDB who you are, so it can decide what it lets you do ?
By default, CouchDB has the following authentication handlers:
- OAuth
- cookie authentication
- HTTP basic authentication, RFC 2617
But wait a minute... To tell the database who I am, I have a choice between OAuth (which isn't documented anywhere, and there doesn't seem to be an actual working example of it, but I assume this was contributed by desktopcouch), cookie authentication (which creates a session and a cookie for later use, but to create the session you need to use a different authentication mechanism in the first place), or basic authentication (which is easy to sniff).
So, in practice, at some point the password is going to be sent over plaintext, and your choice basically is between once, a few times (every time you let your cookie time out, which happens after ten minutes, although you get a new cookie on every request), or every single time. I'm not a security expert, but that doesn't sound good enough.
And typically, my solution would be to switch to https:// and SSL to solve that part. Since CouchDB 1.1 this is included, although I haven't tried it yet (since I'm still on couchdb 1.0.3 because that's what I got working on my phone)
Now, my use case is a command-line application. This adds some additional security and usability concerns:
- When invoking gtd (the mushin command-line client) to add a task, it would be great if I didn't have to specify my username and password every single time. Luckily, gtd can be started as a command line interpreter, so that helps.
- It would be great if I didn't have to specify a password on the command line, either as part of a URL (for example, when replicating) or as an option. I really hate to see passwords either in the process list or in shell history or in a config file, and typically I will use my lowest-quality passwords for apps that force me to do this, and want to avoid writing software that has no other option.
The Plan
In the end, the plan of attack started to clear up:
- Get away from Admin Party in CouchDB, add an admin user
- Create a new user in the _users database in CouchDB, with the same name as my system username
- Create a _security object on the mushin database, and allow my username as a reader.
- to connect to couchdb from gtd, use the current OS user, and ask for the password on the terminal
- Use Paisley's username/password and basic auth support. This means auth details still go over the network in plaintext. Add an Authenticator class that integrates with Paisley such that, when CouchDB refuses the operation, the authenticator can be asked to provide username and password to repeat the same request with. Together with a simple implementation that asks for the password on the terminal, this handles the security problem of passing the password to the application.
- Use the cookie mechanism to avoid sending username/password every time. Create a session using the name and password, then store the cookie, and use that instead for the next request. Anytime you get a new cookie, use that from now on. This was relatively easy to do since paisley has changed to use the new
twisted.web.agent.Agent
and so it was easy to add a cookie-handling Agent together with the cookielib module.
- A tricky bit was replication. When you replicate, you tell one CouchDB server to replicate to or from a database on another CouchDB server - from the point of view of the first one. On the one hand, CouchDB sometimes gives confusing response codes; for example, a 404 in the case where the remote database refuses access to the db, but a 401 in the case where the local database refuses access. On the other hand, we have to give our couchdb database the authentication information for the other one - again, we have to pass username and password, in plaintext, as part of the POST body for replication. I doubt there is a way to do this with cookies or oauth, although I don't know. And in any case, you're not even guaranteed that you can get an oauth token or cookie from the other database, since that database might not even be reachable by you (although this wouldn't be a common case). The best I could do here is, again, ask for the password on the terminal if username is given but password is not.
- Don't log the password anywhere visibly; replace it with asterisks wherever it makes sense (Incidentally, later on I found out that couchdb does exactly the same on its console logging. Excellent.)
- Upgrade to use CouchDB 1.1 everywhere, and do everything over SSL
- Figure out OAuth, possibly stealing techniques from desktopcouch. For a command-line client, it would make sense that my os user is allowed to authenticate to a local couchdb instance only once per, say, X session, and a simple 'gtd add U:5 @home p:mushin add oauth support' would not ask for a password.
I made it pretty far down the list, stopping short at upgrading to couchdb 1.1
But at least tomorrow at work, people will not be able to get at my tasks on their first attempt. (Not that there's anything secret in there anyway, but I digress)