Skip to content

Commit 0abd4ca

Browse files
author
ricardop
committed
completely reworked into an HTTPS_PROXY-based solution
- emit our own certificates - configurable via ENVs - generates config dinamically
1 parent ba4c66e commit 0abd4ca

File tree

7 files changed

+366
-134
lines changed

7 files changed

+366
-134
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
.gitignore
44
LICENSE
55
README.md
6+
docker_mirror_cache
7+
docker_mirror_certs

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
.idea
2+
docker_mirror_cache
3+
docker_mirror_certs

Dockerfile

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
1-
# Use stable nginx on alpine for a light container
2-
FROM nginx:stable-alpine
1+
# We start from my nginx fork which includes the proxy-connect module from tEngine
2+
# Source is available at https://github.com/rpardini/nginx-proxy-connect-stable-alpine
3+
# Its equivalent to nginx:stable-alpine 1.14.0, with alpine 3.7
4+
FROM rpardini/nginx-proxy-connect-stable-alpine:latest
35

4-
# Add openssl and clean apk cache
5-
RUN apk add --update openssl && rm -rf /var/cache/apk/*
6+
# Add openssl, bash and ca-certificates, then clean apk cache -- yeah complain all you want.
7+
RUN apk add --update openssl bash ca-certificates && rm -rf /var/cache/apk/*
68

7-
# Generate a self-signed SSL certificate. It will be ignored by Docker clients due to insecure-registries.
8-
RUN mkdir -p /etc/ssl && \
9-
cd /etc/ssl && \
10-
openssl genrsa -des3 -passout pass:x -out key.pem 2048 && \
11-
cp key.pem key.pem.orig && \
12-
openssl rsa -passin pass:x -in key.pem.orig -out key.pem && \
13-
openssl req -new -key key.pem -out cert.csr -subj "/C=BR/ST=BR/L=Nowhere/O=Fake Docker Mirror/OU=Docker/CN=docker.proxy" && \
14-
openssl x509 -req -days 3650 -in cert.csr -signkey key.pem -out cert.pem
15-
16-
# Create the cache directory
17-
RUN mkdir -p /docker_mirror_cache
9+
# Create the cache directory and CA directory
10+
RUN mkdir -p /docker_mirror_cache /ca
1811

1912
# Expose it as a volume, so cache can be kept external to the Docker image
2013
VOLUME /docker_mirror_cache
2114

15+
# Expose /ca as a volume. Users are supposed to volume mount this, as to preserve it across restarts.
16+
# Actually, its required; if not, then docker clients will reject the CA certificate when the proxy is run the second time
17+
VOLUME /ca
18+
2219
# Add our configuration
2320
ADD nginx.conf /etc/nginx/nginx.conf
2421

25-
# Test that the configuration is OK
26-
RUN nginx -t
22+
# Add our very hackish entrypoint and ca-building scripts, make them executable
23+
ADD entrypoint.sh /entrypoint.sh
24+
ADD create_ca_cert.sh /create_ca_cert.sh
25+
RUN chmod +x /create_ca_cert.sh /entrypoint.sh
26+
27+
# Clients should only use 3128, not anything else.
28+
EXPOSE 3128
29+
30+
## Default envs.
31+
# A space delimited list of registries we should proxy and cache; this is in addition to the central DockerHub.
32+
ENV REGISTRIES="k8s.gcr.io gcr.io quay.io"
33+
# A space delimited list of registry:user:password to inject authentication for
34+
ENV AUTH_REGISTRIES="some.authenticated.registry:oneuser:onepassword another.registry:user:password"
35+
# Should we verify upstream's certificates? Default to true.
36+
ENV VERIFY_SSL="true"
37+
38+
# Did you want a shell? Sorry. This only does one job; use exec /bin/bash if you wanna inspect stuff
39+
ENTRYPOINT ["/entrypoint.sh"]

README.md

Lines changed: 68 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,100 @@
1-
### What?
2-
3-
An intricate, insecure, and hackish way of caching Docker images from private registries (eg, not from DockerHub).
4-
Caches via HTTP man-in-the-middle.
5-
It is highly dependent on Docker-client behavior, and was only tested against Docker 17.03 on Linux (that's the version recommended by Kubernetes 1.10).
6-
7-
#### Why not use Docker's own registry, which has a mirror feature?
8-
9-
Yes, Docker offers [Registry as a pull through cache](https://docs.docker.com/registry/recipes/mirror/),
10-
and, in fact, for a caching solution to be complete, you'll want to run one of those.
11-
12-
**Unfortunately** this only covers the DockerHub case. It won't cache images from `quay.io`, `k8s.gcr.io`, `gcr.io`, or any such, including any private registries.
13-
14-
That means that your shiny new Kubernetes cluster is now a bandwidth hog, since every image will be pulled from the Internet on every Node it runs on, with no reuse.
1+
## docker-registry-proxy
152

16-
This is due to the way the Docker "client" implements `--registry-mirror`, it only ever contacts mirrors for images with no repository reference (eg, from DockerHub).
17-
When a repository is specified `dockerd` goes directly there, via HTTPS (and also via HTTP if included in a `--insecure-registry` list), thus completely ignoring the configured mirror.
3+
### TL,DR
184

19-
_Even worse,_ to complement that client-Docker problem, there is also a one-URL limitation on the registry/mirror side of things, so even if it worked we would need to run multiple mirror-registries, one for each mirrored repo.
20-
21-
22-
#### Hey but that sounds like an important limitation on Docker's side. Shouldn't they fix it?
23-
24-
**Hell, yes**. Actually if you search on Github you'll find a lot of people with the same issues.
25-
* This seems to be the [main issue on the Registry side of things](https://github.com/docker/distribution/issues/1431) and shows a lot of the use cases.
26-
* [Valentin Rothberg](https://github.com/vrothberg) from SUSE has implemented the support
27-
the client needs [in PR #34319](https://github.com/moby/moby/pull/34319) but after a lot of discussions and
28-
[much frustration](https://github.com/moby/moby/pull/34319#issuecomment-389783454) it is still unmerged. Sigh.
5+
A caching proxy for Docker; allows centralized management of registries and their authentication; caches images from *any* registry.
296

7+
### What?
308

31-
**So why not?** I have no idea; it's easy to especulate that "Docker Inc" has no interest in something that makes their main product less attractive. No matter, we'll just _hack_ our way.
9+
Created as an evolution and simplification of [docker-caching-proxy-multiple-private](https://github.com/rpardini/docker-caching-proxy-multiple-private)
10+
using the `HTTPS_PROXY` mechanism and injected CA root certificates instead of `/etc/hosts` hacks and _`--insecure-registry`
3211

33-
### How?
12+
As a bonus it allows for centralized management of Docker registry credentials.
13+
14+
You configure the Docker clients (_err... Kubernetes Nodes?_) once, and then all configuration is done on the proxy --
15+
for this to work it requires inserting a root CA certificate into system trusted root certs.
3416

35-
This solution involves setting up quite a lot of stuff, including DNS hacks.
17+
#### Why not use Docker's own registry, which has a mirror feature?
3618

37-
You'll need a dedicated host for running two caches, both in containers, but you'll need ports 80, 443, and 5000 available.
19+
Yes, Docker offers [Registry as a pull through cache](https://docs.docker.com/registry/recipes/mirror/), *unfortunately*
20+
it only covers the DockerHub case. It won't cache images from `quay.io`, `k8s.gcr.io`, `gcr.io`, or any such, including any private registries.
3821

39-
I'll refer to the caching proxy host's IP address as 192.168.66.62 in the next sections, substitute for your own.
22+
That means that your shiny new Kubernetes cluster is now a bandwidth hog, since every image will be pulled from the
23+
Internet on every Node it runs on, with no reuse.
4024

41-
#### 0) A regular DockerHub registry mirror
25+
This is due to the way the Docker "client" implements `--registry-mirror`, it only ever contacts mirrors for images
26+
with no repository reference (eg, from DockerHub).
27+
When a repository is specified `dockerd` goes directly there, via HTTPS (and also via HTTP if included in a
28+
`--insecure-registry` list), thus completely ignoring the configured mirror.
4229

43-
Just follow instructions on [Registry as a pull through cache](https://docs.docker.com/registry/recipes/mirror/) - expose it on 0.0.0.0:5000.
44-
This will only be used for DockerHub caching, and works well enough.
30+
#### Docker itself should provide this.
4531

46-
#### 1) This caching proxy
32+
Yeah. Docker Inc should do it. So should NPM, Inc. Wonder why they don't. 😼
4733

48-
This is an `nginx` configured extensively for reverse-proxying HTTP/HTTPS to the registries, and apply caching to it.
34+
### Usage
4935

50-
It should be run in a Docker container, and **needs** be mapped to ports 80 and 443. Theres a Docker volume you can mount for storing the cached layers.
36+
- Run the proxy on a dedicated machine.
37+
- Expose port 3128
38+
- Map volume `/docker_mirror_cache` for up to 32gb of cached images from all registries
39+
- Map volume `/ca`, the proxy will store the CA certificate here across restarts
40+
- Env `REGISTRIES`: space separated list of registries to cache; no need to include Docker Hub, its already there
41+
- Env `AUTH_REGISTRIES`: space separated list of `registry:username:password` authentication info. Registry hosts here should be listed in the above ENV as well.
5142

5243
```bash
53-
docker run --rm --name docker_caching_proxy -it \
54-
-p 0.0.0.0:80:80 -p 0.0.0.0:443:443 \
55-
-v /docker_mirror_cache:/docker_mirror_cache \
56-
rpardini/docker-caching-proxy-multiple-private:latest
44+
docker run --rm --name docker_caching_proxy -it \
45+
-p 0.0.0.0:3128:3128 \
46+
-v $(pwd)/docker_mirror_cache:/docker_mirror_cache \
47+
-v $(pwd)/docker_mirror_certs:/ca \
48+
-e REGISTRIES="k8s.gcr.io gcr.io quay.io your.own.registry another.private.registry" \
49+
-e AUTH_REGISTRIES="your.own.registry:username:password another.private.registry:user:pass" \
50+
rpardini/docker-caching-proxy:latest
5751
```
5852

59-
**Important**: the host running the caching proxy container should not have any extra configuration or DNS hacks shown below.
60-
61-
The logging is done to stdout, but the format has been tweaked to show cache MISS/HIT(s) and other useful information for this use case.
53+
Let's say you did this on host `192.168.66.72`, you can then `curl http://192.168.66.72:3128/ca.crt` and get the proxy CA certificate.
6254

63-
It goes to great lengths to try and get the highest hitratio possible, to the point of rewriting headers from registries when they try to redirect to a storage service like Amazon S3 or Google Storage.
55+
#### Configuring the Docker clients / Kubernetes nodes
6456

65-
It is very insecure, anyone with access to the proxy will have access to its cached images regardless of authentication, for example.
57+
On each Docker host that is to use the cache:
6658

59+
- [Configure Docker proxy](https://docs.docker.com/network/proxy/) pointing to the caching server
60+
- Add the caching server CA certificate to the list of system trusted roots.
61+
- Restart `dockerd`
6762

68-
#### 2) dockerd DNS hacks
69-
70-
We'll need to convince Docker (actually, `dockerd` on very host) to talk to our caching proxy via some sort of DNS hack.
71-
The simplest for sure is to just include entries in `/etc/hosts` for each registry you want to mirror, plus a fixed address used for redirects:
63+
Do it all at once, tested on Ubuntu Xenial:
7264

7365
```bash
74-
# /etc/hosts entries for docker caching proxy
75-
192.168.66.72 docker.proxy
76-
192.168.66.72 k8s.gcr.io
77-
192.168.66.72 quay.io
78-
192.168.66.72 gcr.io
79-
```
80-
81-
Only `docker.proxy` is always required, and each registry you want to mirror also needs an entry.
82-
83-
I'm sure you can do stuff to the same effect with your DNS server but I won't go into that.
84-
85-
#### 3) dockerd configuration for mirrors and insecure registries
86-
87-
Of course, we don't have a TLS certificate for `quay.io` et al, so we'll need to tell Docker to treat all proxied registries as _insecure_.
88-
89-
We'll also point Docker to the "regular" registry mirror in item 0.
90-
91-
To do so in one step, edit `/etc/docker/daemon.json` (tested on Docker 17.03 on Ubuntu Xenial only):
92-
93-
```json
94-
{
95-
"insecure-registries": [
96-
"k8s.gcr.io",
97-
"quay.io",
98-
"gcr.io"
99-
],
100-
"registry-mirrors": [
101-
"http://192.168.66.72:5000"
102-
]
103-
}
66+
# Add environment vars pointing Docker to use the proxy
67+
cat << EOD > /etc/systemd/system/docker.service.d/http-proxy.conf
68+
[Service]
69+
Environment="HTTP_PROXY=http://192.168.66.72:3128/"
70+
Environment="HTTPS_PROXY=http://192.168.66.72:3128/"
71+
EOD
72+
73+
# Get the CA certificate from the proxy and make it a trusted root.
74+
curl http://192.168.66.123:3128/ca.crt > /usr/share/ca-certificates/docker_caching_proxy.crt
75+
echo docker_caching_proxy.crt >> /etc/ca-certificates.conf
76+
update-ca-certificates --fresh
77+
78+
# Reload systemd
79+
systemctl daemon-reload
80+
81+
# Restart dockerd
82+
systemctl restart docker.service
10483
```
10584

106-
After that, restart the Docker daemon: `systemctl restart docker.service`
107-
10885
### Testing
10986

110-
Clear the local `dockerd` of everything not currently running: `docker system prune -a -f` (this prunes everything not currently running, beware).
87+
Clear `dockerd` of everything not currently running: `docker system prune -a -f` *beware*
88+
11189
Then do, for example, `docker pull k8s.gcr.io/kube-proxy-amd64:v1.10.4` and watch the logs on the caching proxy, it should list a lot of MISSes.
90+
11291
Then, clean again, and pull again. You should see HITs! Success.
11392

114-
### Gotchas
93+
Do the same for `docker pull ubuntu` and rejoice.
94+
95+
Test your own registry caching and authentication the same way; you don't need `docker login`, or `.docker/config.json` anymore.
11596

116-
Of course, this has a lot of limitations
97+
### Gotchas
11798

118-
- Any HTTP/HTTPS request to the domains of the registries will be proxied, not only Docker calls. *beware*
119-
- If you want to proxy an extra registry you'll have multiple places to edit (`/etc/hosts` and `/etc/docker/daemon.json`) and restart `dockerd` - very brave thing to do in a k8s cluster, so set it up beforehand
120-
- If you authenticate to a private registry and pull through the proxy, those images will be served to any client that can reach the proxy, even without authentication. *beware*
99+
- If you authenticate to a private registry and pull through the proxy, those images will be served to any client that can reach the proxy, even without authentication. *beware*
100+
- Repeat, this will make your private images very public if you're not careful.

create_ca_cert.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#! /bin/bash
2+
3+
set -Eeuo pipefail
4+
5+
declare -i DEBUG=0
6+
7+
logInfo() {
8+
echo "INFO: $@"
9+
}
10+
11+
PROJ_NAME=DockerMirrorBox
12+
logInfo "Will create certificate with names $ALLDOMAINS"
13+
14+
CADATE=$(date "+%Y.%m.%d %H:%M")
15+
CAID="$(hostname -f) ${CADATE}"
16+
17+
CN_CA="${PROJ_NAME} CA Root ${CAID}"
18+
CN_IA="${PROJ_NAME} Intermediate IA ${CAID}"
19+
CN_WEB="${PROJ_NAME} Web Cert ${CAID}"
20+
21+
CN_CA=${CN_CA:0:64}
22+
CN_IA=${CN_IA:0:64}
23+
CN_WEB=${CN_WEB:0:64}
24+
25+
mkdir -p /certs /ca
26+
cd /ca
27+
28+
CA_KEY_FILE=/ca/ca.key
29+
CA_CRT_FILE=/ca/ca.crt
30+
CA_SRL_FILE=/ca/ca.srl
31+
32+
if [ -f "$CA_CRT_FILE" ] ; then
33+
logInfo "CA already exists. Good. We'll reuse it."
34+
else
35+
logInfo "No CA was found. Generating one."
36+
logInfo "*** Please *** make sure to mount /ca as a volume -- if not, everytime this container starts, it will regenerate the CA and nothing will work."
37+
38+
openssl genrsa -des3 -passout pass:foobar -out ${CA_KEY_FILE} 4096
39+
40+
logInfo "generate CA cert with key and self sign it: ${CAID}"
41+
openssl req -new -x509 -days 1300 -sha256 -key ${CA_KEY_FILE} -out ${CA_CRT_FILE} -passin pass:foobar -subj "/C=NL/ST=Noord Holland/L=Amsterdam/O=ME/OU=IT/CN=${CN_CA}" -extensions IA -config <(
42+
cat <<-EOF
43+
[req]
44+
distinguished_name = dn
45+
[dn]
46+
[IA]
47+
basicConstraints = critical,CA:TRUE
48+
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
49+
subjectKeyIdentifier = hash
50+
EOF
51+
)
52+
53+
[[ ${DEBUG} -gt 0 ]] && logInfo "show the CA cert details"
54+
[[ ${DEBUG} -gt 0 ]] && openssl x509 -noout -text -in ${CA_CRT_FILE}
55+
56+
echo 01 > ${CA_SRL_FILE}
57+
58+
fi
59+
60+
cd /certs
61+
62+
logInfo "Generate IA key"
63+
openssl genrsa -des3 -passout pass:foobar -out ia.key 4096 &> /dev/null
64+
65+
logInfo "Create a signing request for the IA: ${CAID}"
66+
openssl req -new -key ia.key -out ia.csr -passin pass:foobar -subj "/C=NL/ST=Noord Holland/L=Amsterdam/O=ME/OU=IT/CN=${CN_IA}" -reqexts IA -config <(
67+
cat <<-EOF
68+
[req]
69+
distinguished_name = dn
70+
[dn]
71+
[IA]
72+
basicConstraints = critical,CA:TRUE,pathlen:0
73+
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
74+
subjectKeyIdentifier = hash
75+
EOF
76+
)
77+
78+
[[ ${DEBUG} -gt 0 ]] && logInfo "Show the singing request, to make sure extensions are there"
79+
[[ ${DEBUG} -gt 0 ]] && openssl req -in ia.csr -noout -text
80+
81+
logInfo "Sign the IA request with the CA cert and key, producing the IA cert"
82+
openssl x509 -req -days 730 -in ia.csr -CA ${CA_CRT_FILE} -CAkey ${CA_KEY_FILE} -out ia.crt -passin pass:foobar -extensions IA -extfile <(
83+
cat <<-EOF
84+
[req]
85+
distinguished_name = dn
86+
[dn]
87+
[IA]
88+
basicConstraints = critical,CA:TRUE,pathlen:0
89+
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
90+
subjectKeyIdentifier = hash
91+
EOF
92+
) &> /dev/null
93+
94+
95+
[[ ${DEBUG} -gt 0 ]] && logInfo "show the IA cert details"
96+
[[ ${DEBUG} -gt 0 ]] && openssl x509 -noout -text -in ia.crt
97+
98+
logInfo "Initialize the serial number for signed certificates"
99+
echo 01 > ia.srl
100+
101+
logInfo "Create the key (w/o passphrase..)"
102+
openssl genrsa -des3 -passout pass:foobar -out web.orig.key 2048 &> /dev/null
103+
openssl rsa -passin pass:foobar -in web.orig.key -out web.key &> /dev/null
104+
105+
logInfo "Create the signing request, using extensions"
106+
openssl req -new -key web.key -sha256 -out web.csr -passin pass:foobar -subj "/C=NL/ST=Noord Holland/L=Amsterdam/O=ME/OU=IT/CN=${CN_WEB}" -reqexts SAN -config <(cat <(printf "[req]\ndistinguished_name = dn\n[dn]\n[SAN]\nsubjectAltName=${ALLDOMAINS}"))
107+
108+
[[ ${DEBUG} -gt 0 ]] && logInfo "Show the singing request, to make sure extensions are there"
109+
[[ ${DEBUG} -gt 0 ]] && openssl req -in web.csr -noout -text
110+
111+
logInfo "Sign the request, using the intermediate cert and key"
112+
openssl x509 -req -days 365 -in web.csr -CA ia.crt -CAkey ia.key -out web.crt -passin pass:foobar -extensions SAN -extfile <(cat <(printf "[req]\ndistinguished_name = dn\n[dn]\n[SAN]\nsubjectAltName=${ALLDOMAINS}")) &> /dev/null
113+
114+
[[ ${DEBUG} -gt 0 ]] && logInfo "Show the final cert details"
115+
[[ ${DEBUG} -gt 0 ]] && openssl x509 -noout -text -in web.crt
116+
117+
logInfo "Concatenating fullchain.pem..."
118+
cat web.crt ia.crt ${CA_CRT_FILE} > fullchain.pem

0 commit comments

Comments
 (0)