Why docker push needs clock to be in sync ?
Few days back I was trying to push to the docker hub and docker push was failing at just
99.x%. I tried to push multiple times but it was exiting with same error message:
Error response from daemon: Get https://registry-1.docker.io/v2/: unauthorized: incorrect username or password
The message was quite confusing to me as it didn’t make any sense to give such error message after pushing upto
99.x%. I tried to look it up on the internet and after visiting lot of links, I saw someone talking about machine’s time in some irrelevant thread. Then it clicked me that sometimes my linux VM’s clock, on which docker runs, gets off the wall clock. Even though ntp runs in the background but it won’t make a big jump to correct the time. So, I manually fixed the time and docker push succeeded.
But I was still wondering why does docker push needs clocks to be in sync ? Any guesses ?The answer is authentication process. I was easily able to find docker docs which describes the authentication process. But why authentication needs time to be in sync ? I’ll answer this up in a while.
I decided to go wild and debug the authentication process manually. From the error message
Get https://registry-1.docker.io/v2/: unauthorized, I saw that docker uses standard HTTPS protocol for authentication. So, I decided to use standard HTTPS MITM proxy to inspect the traffic. I never did MITM on OSX, so wasn’t aware of any good tools for it. Moreover the traffic was originating from my linux VM, that means regular HTTP proxy solutions like burpsuite or charles proxy won’t work. So, I started looking up other options by which I can setup transparent proxy.
squid proxy - I had used it once in past during my internship at ZScaler but it’s quite lengthy process to set it up given that I’m quite lazy.
mitmproxy - Somehow I came across it and I loved its simplicity. I found the doc to setup transparent proxy quite easy. But in this example, they have only mentioned the case in which test device is different than the host on which proxy server is running and test device needs to be configured to use proxy host as the default gateway for sending packets.
Well, I knew I have to do some magic with routing so that I can use the same device as a test device as well as proxying requests. But I wasn’t sure how exactly it has to be done. I tried to look up on the internet and realised I don’t have enough understanding of the linux ip stack. Obviously it’s quite easy to just copy-paste few commands to block/unblock specific ip or port using iptables but how can I route the traffic from the docker instance to proxy server listening on port 8080 - that isn’t possible without proper understanding of how packets go through linux networking stack.
While going through various links on StackOverflow, trying to learn this black magic, I came across linux-ip.net. It explained linux ip stack in quite good detail. So, I decided to step back and learn about it from the beginning. Then I recalled it’s the same site which I had added in my ToDo list a year ago for learning networking on linux.
I skimmed through the whole guide and checked things which seemed relevant enough to me. I might have understood just 20% of it but believe me it was more than enough. I was able to follow and understand all of those SO links.
Above diagram is of utmost importance in case you want to play with the packets using netfilter interface of linux. Now I knew that I have to add rule in
OUTPUT chain to send all the IP packets with destination port of
80/443 (HTTP/HTTPS) to address
127.0.0.1:8080(on which mitmproxy was listening). With this
mitmproxy will be able to listen to all the traffic and show me everything.
iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8080
But after intercepting these packets,
mitmproxy needs to send those packets to original destination. So, it will change the destination address of the packets back to the original address. If that’s the case, then it would result into never ending loop in which my above given rules would send the packets sent by mitmproxy back to mitmproxy itself. So, I needed to do something to break this loop. I decided to change destination of packets sent by processes owned by a “specific” owner.
Basically I added these rules to achieve it:
iptables -t nat -A OUTPUT -p tcp --dport 80 -m owner --uid-owner 1000 -j DNAT --to-destination 127.0.0.1:8080
With this, I ran the mitmproxy as root user(uid = 0). So, my rules will only do DNAT-ing on the packets originated from processes running as
vagrant user(uid=1000). After installing the certs from the
mitm.it, I ran
curl https://google.com and was able to see intercepted traffic in the mitmproxy. But when I ran
docker push, I couldn’t see anything. Then I recalled one difference I had learnt between
docker daemon runs as a root which isn’t safe etc etc. So, basically docker client was sending request to docker daemon and since docker daemon was running as root, it won’t show up in mitmproxy as I was only intercepting packets for
uid=1000 user. I changed the above rules to setup intercepting for root user instead.
iptables -t nat -A OUTPUT -p tcp --dport 80 -m owner --uid-owner 0 -j DNAT --to-destination 127.0.0.1:8080
mitmproxy as non root user and voila!! I was able to see docker’s intercepted traffic. Now, it was time to see the intercepted traffic.
So, first of all it sends request to the
registry-1.docker.io/v2/ and it returns 401 response. Let’s take a look at the full response headers.
Bearer realm in the
Www-Authenticate header in accordance with OAuth format mentioned in https://tools.ietf.org/html/rfc6750#section-3. So, it tells that client has to first authenticate itself with
auth.docker.io and get access token from there using which
registry-1.docker.io will service requests from the client.
Now, let’s check the request and response associated with
In the request headers, docker client has specified
Authorization header passing our credentials to the server and in the response, server returns us
access_token which we have to send back to the
registry-1.docker.io to get access to the resource. That
access_token has associated
issued_at fields which defines for how long this token is valid. These fields are same as
exp fields in jwt specification. Well, that’s the reason why authentication was failing. Though I’m still not sure why docker allowed push upto
99.x% and then failed. Real world is complex for sure.
If you want read more about the authentication process of docker, you can check it out here.
That’s it for now, thanks for reading!