Fun with network namespaces, part 1
Linux has some amazing kernel features to enable containerization. Tools like Docker are built on top of them, and at PythonAnywhere we have built our own virtualization system using them.
One part of these systems that I've not spent much time poking into is network
namespaces. Namespaces are a
general abstraction that allows you to separate out system resources; for example,
if a process is in a mount namespace, then it has its own set of mounted disks
that is separate from those seen by the other processes on a machine -- or if it's in a
process namespace,
then it has its own cordoned-off set of processes visible to it (so,
say, ps auxwf
will just show the ones in its namespace).
As you might expect from that, if you put a process into a network namespace, it will have its own restricted view of what the networking environment looks like -- it won't see the machine's main network interface,
This provides certain advantages when it comes to security, but one that I thought was interesting is that because two processes inside different namespaces would have different networking environments, they could both bind to the same port -- and then could be accessed from outside via port forwarding.
To put that in more concrete terms: my goal was to be able to start up two Flask servers on the same machine, both bound to port 8080 inside their own namespace. I wanted to be able to access one of them from outside by hitting port 6000 on the machine, and the other by hitting port 6001.
Here is a run through how I got that working; it's a lightly-edited set of my "lab notes".
Installing the unifi controller on Arch
This is more of a note-to-self than a proper blog post. I recently got a new Ubiquiti access point, and needed to reinstall the unifi controller on my Arch machine in order to run it.
There's no formal package for unifi, so you have to install the AUR. I use yaourt
for that, and if you do a simple
yaourt -S unifi
...then it will try to install MongoDB from source. According to the Arch Wiki, this requires "180GB+ free disk space, and may take several hours to build (i.e. 6.5 hours on Intel i7, 1 hour on 32 Xeon cores with high-end NVMe.)". So not ideal.
The trick is to install MongoDB from binary first:
yaourt -S mongodb-bin
And only after that:
yaourt -S unifi
Finally, activate the service:
sudo systemctl enable unifi
sudo systemctl start unifi
...and then go to https://localhost:8443/
, accept the self-signed cert, and you're all set.
pam-unshare: a PAM module that switches into a PID namespace
Today in my 10% time at PythonAnywhere (we're
a bit less lax than Google) I wrote
a PAM module that lets you configure a
Linux system so that when someone su
s, sudo
s, or ssh
es in, they are put
into a private PID namespace. This means that they can't see anyone else's
processes, either via ps
or via /proc
. It's definitely not production-ready,
but any feedback on it would be very welcome.
In this blog post I explain why I wrote it, and how it all works, including some of the pitfalls of using PID namespaces like this and how I worked around them.
SHA-1 sunset in Chromium, and libnss3
This post is a combination of a description of a Chrome bug (fixed in May), a mea culpa, and an explanation of of the way HTTPS certificates work. So there's something for everyone! :-)
Here's the situation -- don't worry if you don't understand all of this initially, a lot of it is explained later. Last year, the Chromium team decided that they should encourage site owners to stop using HTTPS certificates signed using the SHA-1 algorithm, which has security holes. The way they are doing this is by making the "padlock" icon in the URL bar show that a site is not secure if it has a certificate that expires after the end of 2015 if either the certificate itself is signed with SHA-1, or if any of the certificates in its chain are. I encountered some weird behaviour related to this when we recently got a new certificate for PythonAnywhere. Hopefully by posting about it here (with a bit of background covering the basics of how certificates work, including some stuff I learned along the way) I can help others who encounter the same problem.
tl;dr for people who understand certificates in some depth -- if any certificate in your chain, including your own cert, is signed with multiple hashing algorithms, then if you're using Chrome and have a version of libnss < 3.17.4 installed, Chrome's check to warn about SHA-1 signatures, instead of looking at the most-secure signature for each cert, will look at the least-secure one. So your certificate will look like it's insecure even if it's not. Solution for Ubuntu (at least for 14.04 LTS):
sudo apt-get install libnss3
. Thank you so much to Vincent G on Server Fault for working out the fix.
Here's the background. It's simplified a bit, but I think is largely accurate -- any corrections from people who know more about this stuff than I do would be much appreciated!
Public/private keys
Most readers here probably have a decent working knowledge of asymmetrical cryptography, so I'm going to skip a lot of detail here; there are excellent primers on public key encryption all over the Internet and if you need to know more, google for one that suits you.
But if you just want to get through this post, here's the stuff you need to know: public and private keys are large numbers, generated in public/private pairs. Each of them can be used on its own to encrypt data. Stuff that is encrypted with the private key can be decrypted with the public key, and vice versa.
It is almost impossibly unlikely that a particular bit of data encrypted with one private key would be the same as the same data encrypted with a different private key. So, if I want to prove that I sent you something, and you already have a copy of my public key (let's ignore how you got hold of it for now), I can send you the data encrypted with my private key, and if you can decrypt it then it's pretty much guaranteed that it was me who sent it -- or at least it was someone who has a copy of my private key.
Furthermore, we can extend this to a concept of digital signatures. If I want to send you some data and to prove that it came from me, I can use a hash function to reduce that data down to a short(ish) number, then I can encrypt that hash with my private key. I then send you the data, with the encrypted hash tacked onto the end as a signature. To verify that it really came from me, you hash the data using the same algorithm, then decrypt the signature using my public key. If they match, then you know I really did sign it.
This has a couple of advantages over simply encrypting the whole thing using my private key -- the most relevant for this post being that the data itself is in the clear: we're only using the encryption to prove who provided it.
Certificates
When you want to run an HTTPS site, you need to get a certificate. This is basically some data that states the domain that's covered by the certificate and who owns it, and a public key. It basically claims "the owner of the private key associated with this public key is the person that this data relates to". It also has some data saying "this is who says that this claim is true" -- the issuer. So, for the certificate that you bought from, say, Joe's SSL Certificates, the issuer will be some identifier for that company.
So, the question is, how do we stop people from just issuing themselves certificates saying that Joe has said that the private/public key pair they've just generated is the correct one for google.com? The certificate is digitally signed using Joe's SSL Certificates' private key. So, assuming that the browser has Joe's SSL Certificates' public key, and it trusts Joe to only sign certificates that he really knows are OK, it just uses Joe's public key to validate the signature.
Browsers come with a bunch of public keys installed as ones they should trust (actually, some rely on a list provided by the operating system). They're called "root CAs", and in this case, we're saying that Joe's SSL Certificates is one of them. In the real world, maybe not every browser in the world does trust the specific issuer who signed your certificate.
Certificate chains
What happens under these circumstances is that Joe gets his own certificate. This includes his public key, and is signed by someone else. So, the browser receives your certificate and Joe's when they visit your website. They check your certificate against Joe's, using the public key in Joe's certificate, and then they check Joe's against the certificate they have stored for whoever signed Joe's one.
And obviously this can go on and on; perhaps Joe's certificate was signed by Amy, who your browser doesn't trust... so your web server has to send your certificate, and Joe's certificate, and Amy's certificate, and Amy's certificate is signed by someone the browser does trust and we're done.
This could in theory go on pretty much indefinitely, with hundreds of certificates being sent, giving a chain of trust from your certificate up to someone the browser really does trust -- that is, back to an issuer and their associated public key that came packaged with Chrome or Firefox or whatever browser is being used. But in practice, chains are normally between two and five certificates long.
So, if you've ever set up SSL for a domain on PythonAnywhere or on your own server, now you know what all that stuff with the "certificate chain" or "bundle" was. You were putting together a set of certificates that started with your own one, then went from there across a sequence of signatures and public keys to one that all web browsers trust by default.
Signature hashing algorithms
One thing we've glossed over a bit until now is the hashing algorithm. It's really important that different certificates hash to different numbers. Let's imagine we had a somewhat naive algorithm:
def hash(certificate):
return 4
I could get a certificate issued for my own domain, which would include as its signature the number 4 encrypted by my issuer's private key. Because I know that every other certificate in the world also hashes to the number 4, I can just change my certificate so that it says that it relates to google.com, keeping the same public key, and it will still pass the signature validation browsers do. This means that if I can somehow trick your computer (through some kind of clever network hackery) into thinking that my website is google.com, then I can present my hacked certificate when you visit, and your browser will accept it and show you a green padlock in the URL bar. This is obviously not a Good Thing.
This is where we can actually start talking about SHA-1. It's a hashing algorithm designed by the NSA back in 1995. It's no longer regarded as being very good -- initial problems started surfacing in 2005. This is for cryptographic values of "not very good" -- it's still pretty much impossible to produce a valid certificate that would hash to the same value as one that you're trying to impersonate. But it's possible in theory, and has been for quite a while, so it's time to move on to SHA-2. SHA-2 is also an NSA creation -- it's worth noting that the Snowden revelations don't seem to have done the hashing algorithm's reputation any harm, and it's still regarded as the way to go.
(Just to confuse matters a bit, SHA-2 is actually a family of hash functions, called SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, and SHA-512/256. So if someone is talking about something hashed with SHA-256, they could equally, if less precisely, say that it was hashed with SHA-2. I'll try to use SHA-2 in the remainder of this blog post to stop my own brain from melting when I reread it...)
Sign it once, sign it twice
So there are different hash algorithms that can be used to sign things. But that obviously could cause problems -- given a particular signature, how does the person checking the signature know which one was used? The simple fix is to add an extra field to the signature saying which algorithm was used.
And what if the checker doesn't know how to perform the particular hash that the signature requires? After all, this is the Internet, and there are computers running software of all different ages knocking around. The answer is that each certificate can have multiple signatures, all using different hashing algorithms but encrypted with the same private key (and so, decryptable with the same public key). It's simple to do -- hash the data once using SHA-1, encrypt that with your private key, and store that as the SHA-1 signature. Then hash the original data again using SHA-2, encrypt the result with the same private key, and then store that as the SHA-2 one.
Once that's done, a browser trying to validate a certificate can go through the signatures, and find the most secure signature it understands the hashing algorithm for. Then it can check that one and ignore the rest. Old software that doesn't understand the latest and greatest hashing algorithms has lower security than new software, but it'll still have something. And new software can stay ahead and always choose the newest algorithm.
The sunset
So, SHA-1 bad, SHA-2 good. Google quite understandably wanted to reduce the number of sites using certificates using the old, broken encryption. So they decided that when your browser loads a website, it will look along the chain and see if there are any certificates there that are SHA-1 only. If it's expiring soon (as in, before the end of 2015), they'll accept it anyway. That means that no-one with an old SHA-1 certificate was forced to upgrade quickly. But if it expires later than that, then they show a "broken security" warning. If you click on that, it tells you that one of the certificates in the chain uses SHA-1 so it's not secure.
The problem
Phew! Finally I can tell you what triggered this whole infodump :-)
We recently got a new certificate for PythonAnywhere. Our current one expires in August 2015, so we needed a new one for another year. I was checking it out prior to pushing it to our live service, and got the "broken security" warning. "Tsk", I thought, "what a useless certificate issuer we have!" A bit of googling around led me to pages that seemed to be claiming that our issuer had a habit of issuing SHA-1 certs and you had to kick them to get an SHA-2 one. So I sent them a grumpy email asking for an SHA-2 version of our cert, or whichever one in the chain it was. (At this point I didn't realise that a certificate could have multiple signatures, and -- I think -- the version of Chrome I was using to explore the certificate chain on the installed certificate on the test servers only showed me the SHA-1 signatures when I checked.)
Then a client provided us with a new cert for their own domain. We installed it, and bang -- they got the same warning. I let them know about what the problem appeared to me to be. But after a couple of back-and-forwards with their certificate issuers, mediated by them (and I have to thank them for their patience in this) I started doubting myself. My computer was showing the SHA-1 errors in Chrome. A Crunchbang virtual machine I was using, likewise. But the client themselves couldn't see it. They checked on Firefox, but Mozilla aren't doing this sunsetting thing, so that was expected. But then they checked in Chrome on Windows, and they didn't get it. The same Chrome version as I was running, but no warning.
The clincher for me was when my colleague Glenn checked it in his browser, running on Ubuntu, just like me. Same Chrome version... no error. There was obviously something wrong with my own machine! A whole bunch of Googling later and I found this answer to a Server Fault post, by Vincent G.
There's an Ubuntu library, libnss3, described as "Network Security Service libraries". In version 3.17.4, there was a fix to a bug described as "NSS incorrectly preferring a longer, weaker chain over a shorter, stronger chain". It looks like this was pushed live in May.
I use i3 as my window manager, which means that the normal Ubuntu "you have updates you need to install" stuff doesn't happen, so I need to update my OS manually. It looks like it was a while since I did that... (Cue spearphishing attacks.)
I updated, and suddenly both our certificate and the client's looked OK. Sincere apologies emailed to both clients and to our respective CAs...
So, just to reiterate what happened... both we and our clients were issued with new certificates. These expired after the end of 2015. Each certificate in the chain up to a root cert was signed with SHA-2 hashing, and also (for backward compatibility) was also signed with SHA-1. When loaded into a Chrome with no buggy libraries, the browser would look along the chain and recognise that every certificate had an SHA-2 signature, so it would decide it was fine. But in my version with the buggy libnss3, it would look along the chain and spot the SHA-1 signatures. It would erroneously decide to ignore the SHA-2 ones, and would report the certificate as broken.
The moral of the story? Keep updating your system. And if something looks
broken, check it on as many OS/browser combinations as possible... On the other
hand, when select
really is broken, it's a real pain to debug.
...just resting...
Just a quick note to say that I'm still here! Using rsp as a front-end for this site has usefully shown up some weird bugs, and I'm tracking them down. I'll do a new post about it when there's something useful to say...
Writing a reverse proxy/loadbalancer from the ground up in C, part 4: Dealing with slow writes to the network
This is the fourth step along my road to building a simple C-based reverse proxy/loadbalancer, rsp, so that I can understand how nginx/OpenResty works -- more background here. Here are links to the first part, where I showed the basic networking code required to write a proxy that could handle one incoming connection at a time and connect it with a single backend, to the second part, where I added the code to handle multiple connections by using epoll, and to the third part, where I started using Lua to configure the proxy.
This post was was unplanned; it shows how I fixed a bug that I discovered when I first tried to use rsp to act as a reverse proxy in front of this blog. The bug is fixed, and you're now reading this via rsp. The problem was that when the connection from a browser to the proxy was slower than the connection from the proxy to the backend (that is, most of the time), then when new data was received from the backend and we tried to send it to the client, we sometimes got an error to tell us that the client was not ready. This error was being ignored, so a block of data would be skipped, so the pages you got back would be missing chunks. There's more about the bug here.
A brief sidetrack: Varnish
In order to use this blog as a decent real-world test of rsp, I figured that I should make it as fast as possible. The quickest way to do that was to install Varnish, which is essentially a reverse proxy that caches stuff. You configure it to say what is cachable, and then it runs in place of the web server and proxies anything it can't cache back to it.
I basically used the instructions from Ewan Leith's excellent "10 Million hits a day with Wordpress using a $15 server" post.
So now, this server has:
- rsp running on port 80, proxying everything to port 83.
- varnish running on port 83, caching what it can and proxying the rest to port 81.
- nginx running on port 81, serving static pages and sending PHP stuff to php5-fpm on port 9000.
I've also got haproxy running on port 82, doing the same as rsp -- proxying everything to varnish -- so that I can do some comparative speed tests once rsp does enough for such tests to give interesting results. Right now, all of the speed differences seem to be in the noise, with a run of ab pointed at varnish actually coming out slower than the two proxies.
Writing a reverse proxy/loadbalancer from the ground up in C, pause to regroup: fixed it!
It took a bit of work, but the bug is fixed: rsp now handles correctly the case when it can't write as much as it wants to the client side. I think this is enough for it to properly work as a front-end for this website, so it's installed and running here. If you're reading this (and I've not had to switch it off in the meantime) then the pages you're reading were served over rsp. Which is very pleasing :-)
The code needs a bit of refactoring before I can present it, and the same bug still exists on the communicating-to-backends side (which is one of the reasons it needs refactoring -- this is something I should have been able to fix in one place only) so I'll do that over the coming days, and then do another post.
Writing a reverse proxy/loadbalancer from the ground up in C, pause to regroup: non-blocking output
Before moving on to the next step in my from-scratch reverse proxy, I thought it would be nice to install it on the machine where this blog runs, and proxy all access to the blog through it. It would be useful dogfooding and might show any non-obvious errors in the code. And it did.
I found that while short pages were served up perfectly well, longer pages were corrupted and interrupted halfway through. Using curl gave various weird errors, eg. curl: (56) Problem (3) in the Chunked-Encoded data
, which is a general error saying that it's receiving chunked data and the chunking is invalid.
Doubly strangely, these problems didn't happen when I ran the proxy on the machine where I'm developing it and got it to proxy the blog; only when I ran it on the same machine as the blog. They're different versions of Ubuntu, the blog server being slightly older, but not drastically so -- and none of the stuff I'm using is that new, so it seemed unlikely to be a bug in the blog server's OS. And anyway, select isn't broken.
After a ton of debugging with printf
s here there and everywhere, I tracked it down. You'll remember that our code to transfer data from the backend to the client looks like this:
void handle_backend_socket_event(struct epoll_event_handler* self, uint32_t events) { struct backend_socket_event_data* closure = (struct backend_socket_event_data*) self->closure;char buffer[BUFFER_SIZE]; int bytes_read;
if (events & EPOLLIN) { bytes_read = read(self->fd, buffer, BUFFER_SIZE); if (bytes_read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { return; }
if (bytes_read == 0 || bytes_read == -1) { close_client_socket(closure->client_handler); close_backend_socket(self); return; }
write(closure->client_handler->fd, buffer, bytes_read); }
if ((events & EPOLLERR) | (events & EPOLLHUP) | (events & EPOLLRDHUP)) { close_client_socket(closure->client_handler); close_backend_socket(self); return; }
}
If you look closely, there's a system call there where I'm not checking the return value -- always risky. It's this:
write(closure->client_handler->fd, buffer, bytes_read);
The write
function returns the number of bytes it managed to write, or an error code. The debugging code revealed that sometimes it was returning -1, and errno
was set to EAGAIN, meaning that the operation would have blocked on a non-blocking socket.
This makes a lot of sense. Sending stuff out over the network is a fairly complex process. There are kernel buffers of stuff to send, and as we're using TCP, which is connection-based, I imagine there's a possibility that the client being slow or transmission of data over the Internet might be causing things to back up. Possibly sometimes it was returning a non-error code, too, but was still not able to write all of the bytes I asked it to write, so stuff was getting skipped.
So that means that even for this simple example of an epoll-based proxy to work properly, we need to do some kind of buffering in the server to handle cases where we're getting stuff from the backend faster than we can send it to the client. And possibly vice versa. It's possible to get epoll events on an FD when it's ready to accept output, so that's probably the way to go -- but it will need a bit of restructuring. So the next step will be to implement that, rather than the multiple-backend handling stuff I was planning.
This is excellent. Now I know a little more about why writing something like nginx is hard, and have a vague idea of why I sometimes see stuff in its logs along the lines of an upstream response is buffered to a temporary file
. Which is entirely why I started writing this stuff in the first place :-)
Here's a run-through of the code I had to write to fix the bug.
Writing a reverse proxy/loadbalancer from the ground up in C, part 3: Lua-based configuration
This is the third step along my road to building a simple C-based reverse proxy/loadbalancer so that I can understand how nginx/OpenResty works -- more background here. Here's a link to the first part, where I showed the basic networking code required to write a proxy that could handle one incoming connection at a time and connect it with a single backend, and to the second part, where I added the code to handle multiple connections by using epoll.
This post is much shorter than the last one. I wanted to make the minimum changes to introduce some Lua-based scripting -- specifically, I wanted to keep the same proxy with the same behaviour, and just move the stuff that was being configured via command-line parameters into a Lua script, so that just the name of that script would be specified on the command line. It was really easy :-) -- but obviously I may have got it wrong, so as ever, any comments and corrections would be much appreciated.