diff --git a/Dockerfile b/Dockerfile index 53065ff..1a2d0a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,11 @@ LABEL org.opencontainers.image.source https://github.com/rpardini/docker-registr RUN apk add --no-cache --update bash ca-certificates-bundle coreutils openssl # If set to 1, enables building mitmproxy, which helps a lot in debugging, but is super heavy to build. -ARG DEBUG_BUILD="1" +ARG DEBUG_BUILD="0" ENV DO_DEBUG_BUILD="$DEBUG_BUILD" # Build mitmproxy via pip. This is heavy, takes minutes do build and creates a 90mb+ layer. Oh well. -RUN [[ "a$DO_DEBUG_BUILD" == "a1" ]] && { echo "Debug build ENABLED." \ +RUN [ "$DO_DEBUG_BUILD" = "1" ] && { echo "Debug build ENABLED." \ && apk add --no-cache --update su-exec git g++ libffi libffi-dev libstdc++ openssl-dev python3 python3-dev py3-pip py3-wheel py3-six py3-idna py3-certifi py3-setuptools \ && LDFLAGS=-L/lib pip install mitmproxy==5.2 \ && apk del --purge git g++ libffi-dev openssl-dev python3-dev py3-pip py3-wheel \ @@ -28,7 +28,7 @@ RUN [[ "a$DO_DEBUG_BUILD" == "a1" ]] && { echo "Debug build ENABLED." \ ENV LANG=en_US.UTF-8 # Check the installed mitmproxy version, if built. -RUN [[ "a$DO_DEBUG_BUILD" == "a1" ]] && { mitmproxy --version && mitmweb --version ; } || { echo "Debug build disabled."; } +RUN [ "$DO_DEBUG_BUILD" = "1" ] && { mitmproxy --version && mitmweb --version ; } || { echo "Debug build disabled."; } # Create the cache directory and CA directory RUN mkdir -p /docker_mirror_cache /ca @@ -60,17 +60,24 @@ EXPOSE 8082 ## Default envs. # A space delimited list of registries we should proxy and cache; this is in addition to the central DockerHub. -ENV REGISTRIES="k8s.gcr.io gcr.io quay.io" +ENV REGISTRIES="auth.docker.io registry-1.docker.io docker.caching.proxy.internal k8s.gcr.io gcr.io quay.io gitlab.com registry.gitlab.com" # A space delimited list of registry:user:password to inject authentication for -ENV AUTH_REGISTRIES="some.authenticated.registry:oneuser:onepassword another.registry:user:password" +# (e.g. AUTH_REGISTRIES="auth.docker.io:dhuser:dhpass gitlab.com:gluser:glpass") +ENV AUTH_REGISTRIES="" # Should we verify upstream's certificates? Default to true. ENV VERIFY_SSL="true" + # Enable debugging mode; this inserts mitmproxy/mitmweb between the CONNECT proxy and the caching layer ENV DEBUG="false" # Enable debugging mode; this inserts mitmproxy/mitmweb between the caching layer and DockerHub's registry ENV DEBUG_HUB="false" # Enable nginx debugging mode; this uses nginx-debug binary and enabled debug logging, which is VERY verbose so separate setting ENV DEBUG_NGINX="false" +# Enable debugging mode for creating CA certificate +ENV DEBUG_CA_CERT="false" + +# Set Docker Registry cache size, by default, 32 GB ('32g') +ENV CACHE_MAX_SIZE="32g" # Manifest caching tiers. Disabled by default, to mimick 0.4/0.5 behaviour. # Setting it to true enables the processing of the ENVs below. @@ -94,8 +101,13 @@ ENV MANIFEST_CACHE_SECONDARY_TIME="60d" # In the default config, :latest and other frequently-used tags will get this value. ENV MANIFEST_CACHE_DEFAULT_TIME="1h" +# Should we allow overridding with own authentication, default to false. +ENV ALLOW_OWN_AUTH="false" + # Should we allow actions different than pull, default to false. ENV ALLOW_PUSH="false" +# Should we allow push only with own authentication, default to false. +ENV ALLOW_PUSH_WITH_OWN_AUTH="false" # Timeouts # ngx_http_core_module diff --git a/README.md b/README.md index a419e99..e92f911 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ for this to work it requires inserting a root CA certificate into system trusted - Expose port 3128 to the network - Map volume `/docker_mirror_cache` for up to `CACHE_MAX_SIZE` (32gb by default) of cached images across all cached registries - Map volume `/ca`, the proxy will store the CA certificate here across restarts. **Important** this is security sensitive. -- Env `ALLOW_PUSH` : This bypasses the proxy when pushing, default to false - if kept to false, pushing will not work. For more info see this [commit](https://github.com/rpardini/docker-registry-proxy/commit/536f0fc8a078d03755f1ae8edc19a86fc4b37fcf). +- Env `ALLOW_OWN_AUTH` (default `false`): Allow overridding the `AUTH_REGISTRIES` authentication with own Docker credentials if provided (to support `docker login` as another user). +- Env `ALLOW_PUSH` (default `false`): This bypasses the proxy when pushing, default to false - if kept to false, pushing will not work. For more info see this [commit](https://github.com/rpardini/docker-registry-proxy/commit/536f0fc8a078d03755f1ae8edc19a86fc4b37fcf). +- Env `ALLOW_PUSH_WITH_OWN_AUTH` (default `false`): Allow bypassing the proxy when pushing only if own authentication is provided. - Env `CACHE_MAX_SIZE` (default `32g`): set the max size to be used for caching local Docker image layers. Use [Nginx sizes](http://nginx.org/en/docs/syntax.html). - Env `ENABLE_MANIFEST_CACHE`, see the section on pull rate limiting. - Env `REGISTRIES`: space separated list of registries to cache; no need to include DockerHub, its already done internally. diff --git a/create_ca_cert.sh b/create_ca_cert.sh index b7c9352..c27099b 100644 --- a/create_ca_cert.sh +++ b/create_ca_cert.sh @@ -2,8 +2,6 @@ set -Eeuo pipefail -declare -i DEBUG=0 - logInfo() { echo "INFO: $@" } @@ -53,10 +51,10 @@ subjectKeyIdentifier = hash EOF ) - [[ ${DEBUG} -gt 0 ]] && logInfo "show the CA cert details" - [[ ${DEBUG} -gt 0 ]] && openssl x509 -noout -text -in ${CA_CRT_FILE} + [ "${DEBUG_CA_CERT}" = "true" ] && logInfo "show the CA cert details" + [ "${DEBUG_CA_CERT}" = "true" ] && openssl x509 -noout -text -in ${CA_CRT_FILE} - echo 01 > ${CA_SRL_FILE} + echo "01" > ${CA_SRL_FILE} fi @@ -78,8 +76,8 @@ subjectKeyIdentifier = hash EOF ) -[[ ${DEBUG} -gt 0 ]] && logInfo "Show the singing request, to make sure extensions are there" -[[ ${DEBUG} -gt 0 ]] && openssl req -in ia.csr -noout -text +[ "${DEBUG_CA_CERT}" = "true" ] && logInfo "Show the singing request, to make sure extensions are there" +[ "${DEBUG_CA_CERT}" = "true" ] && openssl req -in ia.csr -noout -text logInfo "Sign the IA request with the CA cert and key, producing the IA cert" openssl x509 -req -days 730 -in ia.csr -CA ${CA_CRT_FILE} -CAkey ${CA_KEY_FILE} -CAserial ${CA_SRL_FILE} -out ia.crt -passin pass:foobar -extensions IA -extfile <( @@ -95,8 +93,8 @@ EOF ) &> /dev/null -[[ ${DEBUG} -gt 0 ]] && logInfo "show the IA cert details" -[[ ${DEBUG} -gt 0 ]] && openssl x509 -noout -text -in ia.crt +[ "${DEBUG_CA_CERT}" = "true" ] && logInfo "show the IA cert details" +[ "${DEBUG_CA_CERT}" = "true" ] && openssl x509 -noout -text -in ia.crt logInfo "Initialize the serial number for signed certificates" echo 01 > ia.srl @@ -108,14 +106,14 @@ openssl rsa -passin pass:foobar -in web.orig.key -out web.key &> /dev/null logInfo "Create the signing request, using extensions" 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}")) -[[ ${DEBUG} -gt 0 ]] && logInfo "Show the singing request, to make sure extensions are there" -[[ ${DEBUG} -gt 0 ]] && openssl req -in web.csr -noout -text +[ "${DEBUG_CA_CERT}" = "true" ] && logInfo "Show the singing request, to make sure extensions are there" +[ "${DEBUG_CA_CERT}" = "true" ] && openssl req -in web.csr -noout -text logInfo "Sign the request, using the intermediate cert and key" 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 -[[ ${DEBUG} -gt 0 ]] && logInfo "Show the final cert details" -[[ ${DEBUG} -gt 0 ]] && openssl x509 -noout -text -in web.crt +[ "${DEBUG_CA_CERT}" = "true" ] && logInfo "Show the final cert details" +[ "${DEBUG_CA_CERT}" = "true" ] && openssl x509 -noout -text -in web.crt logInfo "Concatenating fullchain.pem..." cat web.crt ia.crt ${CA_CRT_FILE} > fullchain.pem diff --git a/entrypoint.sh b/entrypoint.sh index 7a7aa2c..2c3e596 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,7 +5,7 @@ trap "echo TRAPed signal" HUP INT QUIT TERM #configure nginx DNS settings to match host, why must we do that nginx? export RESOLVERS=$(awk '$1 == "nameserver" {print ($2 ~ ":")? "["$2"]": $2}' ORS=' ' /etc/resolv.conf | sed 's/ *$//g') -if [ "x$RESOLVERS" = "x" ]; then +if [ -z "$RESOLVERS" ]; then echo "Warning: unable to determine DNS resolvers for nginx" >&2 exit 66 fi @@ -20,7 +20,7 @@ done echo "Final chosen resolver: $conf" confpath=/etc/nginx/resolvers.conf -if [ ! -e $confpath ] || [ "$conf" != "$(cat $confpath)" ] +if [ ! -e "$confpath" ] || [ "$conf" != "$(cat "$confpath")" ] then echo "Using auto-determined resolver '$conf' via '$confpath'" echo "$conf" > $confpath @@ -36,7 +36,7 @@ ALLDOMAINS="" echo -n "" > /etc/nginx/docker.intercept.map # Some hosts/registries are always needed, but others can be configured in env var REGISTRIES -for ONEREGISTRYIN in docker.caching.proxy.internal registry-1.docker.io auth.docker.io ${REGISTRIES}; do +for ONEREGISTRYIN in ${REGISTRIES}; do ONEREGISTRY=$(echo ${ONEREGISTRYIN} | xargs) # Remove whitespace echo "Adding certificate for registry: $ONEREGISTRY" ALLDOMAINS="${ALLDOMAINS},DNS:${ONEREGISTRY}" @@ -55,7 +55,7 @@ echo -n "" > /etc/nginx/docker.targetHost.map echo -n "" > /etc/nginx/docker.auth.map # Only configure auth registries if the env var contains values -if [ "$AUTH_REGISTRIES" ]; then +if [ -n "$AUTH_REGISTRIES" ]; then # Ref: https://stackoverflow.com/a/47633817/219530 AUTH_REGISTRIES_DELIMITER=${AUTH_REGISTRIES_DELIMITER:-" "} s=$AUTH_REGISTRIES$AUTH_REGISTRIES_DELIMITER @@ -87,9 +87,6 @@ fi echo " listen 443 ssl default_server;" > /etc/nginx/caching.layer.listen echo "error_log /var/log/nginx/error.log warn;" > /etc/nginx/error.log.debug.warn -# Set Docker Registry cache size, by default, 32 GB ('32g') -CACHE_MAX_SIZE=${CACHE_MAX_SIZE:-32g} - # The cache directory. This can get huge. Better to use a Docker volume pointing here! # Set to 32gb which should be enough echo "proxy_cache_path /docker_mirror_cache levels=1:2 max_size=$CACHE_MAX_SIZE inactive=60d keys_zone=cache:10m use_temp_path=off;" > /etc/nginx/conf.d/cache_max_size.conf @@ -97,47 +94,57 @@ echo "proxy_cache_path /docker_mirror_cache levels=1:2 max_size=$CACHE_MAX_SIZE # Manifest caching configuration. We generate config based on the environment vars. echo -n "" >/etc/nginx/nginx.manifest.caching.config.conf -[[ "a${ENABLE_MANIFEST_CACHE}" == "atrue" ]] && [[ "a${MANIFEST_CACHE_PRIMARY_REGEX}" != "a" ]] && cat <>/etc/nginx/nginx.manifest.caching.config.conf +if [ "${ENABLE_MANIFEST_CACHE}" = "true" ]; then + [ -n "${MANIFEST_CACHE_PRIMARY_REGEX}" ] && cat <>/etc/nginx/nginx.manifest.caching.config.conf # First tier caching of manifests; configure via MANIFEST_CACHE_PRIMARY_REGEX and MANIFEST_CACHE_PRIMARY_TIME location ~ ^/v2/(.*)/manifests/${MANIFEST_CACHE_PRIMARY_REGEX} { set \$docker_proxy_request_type "manifest-primary"; proxy_cache_valid ${MANIFEST_CACHE_PRIMARY_TIME}; include "/etc/nginx/nginx.manifest.stale.conf"; } -EOD - -[[ "a${ENABLE_MANIFEST_CACHE}" == "atrue" ]] && [[ "a${MANIFEST_CACHE_SECONDARY_REGEX}" != "a" ]] && cat <>/etc/nginx/nginx.manifest.caching.config.conf +EOF + [ -n "${MANIFEST_CACHE_SECONDARY_REGEX}" ] && cat <>/etc/nginx/nginx.manifest.caching.config.conf # Secondary tier caching of manifests; configure via MANIFEST_CACHE_SECONDARY_REGEX and MANIFEST_CACHE_SECONDARY_TIME location ~ ^/v2/(.*)/manifests/${MANIFEST_CACHE_SECONDARY_REGEX} { set \$docker_proxy_request_type "manifest-secondary"; proxy_cache_valid ${MANIFEST_CACHE_SECONDARY_TIME}; include "/etc/nginx/nginx.manifest.stale.conf"; } -EOD - -[[ "a${ENABLE_MANIFEST_CACHE}" == "atrue" ]] && cat <>/etc/nginx/nginx.manifest.caching.config.conf +EOF + cat <>/etc/nginx/nginx.manifest.caching.config.conf # Default tier caching for manifests. Caches for ${MANIFEST_CACHE_DEFAULT_TIME} (from MANIFEST_CACHE_DEFAULT_TIME) location ~ ^/v2/(.*)/manifests/ { set \$docker_proxy_request_type "manifest-default"; proxy_cache_valid ${MANIFEST_CACHE_DEFAULT_TIME}; include "/etc/nginx/nginx.manifest.stale.conf"; } -EOD - -[[ "a${ENABLE_MANIFEST_CACHE}" != "atrue" ]] && cat <>/etc/nginx/nginx.manifest.caching.config.conf +EOF +else + cat <>/etc/nginx/nginx.manifest.caching.config.conf # Manifest caching is disabled. Enable it with ENABLE_MANIFEST_CACHE=true location ~ ^/v2/(.*)/manifests/ { set \$docker_proxy_request_type "manifest-default-disabled"; proxy_cache_valid 0s; include "/etc/nginx/nginx.manifest.stale.conf"; } -EOD +EOF +fi echo -e "\nManifest caching config: ---\n" cat /etc/nginx/nginx.manifest.caching.config.conf echo "---" -if [[ "a${ALLOW_PUSH}" == "atrue" ]]; then +echo -n "" > /etc/nginx/conf.d/allowed_override_auth.conf +if [ "${ALLOW_OWN_AUTH}" = "true" ]; then + cat <<'EOF' > /etc/nginx/conf.d/allowed_override_auth.conf + if ($http_authorization != "") { + # override with own authentication if provided + set $finalAuth $http_authorization; + } +EOF +fi + +if [ "${ALLOW_PUSH}" = "true" ]; then cat < /etc/nginx/conf.d/allowed.methods.conf # allow to upload big layers client_max_body_size 0; @@ -145,6 +152,31 @@ if [[ "a${ALLOW_PUSH}" == "atrue" ]]; then # only cache GET requests proxy_cache_methods GET; EOF +elif [ "${ALLOW_PUSH_WITH_OWN_AUTH}" = "true" ]; then + cat <<'EOF' > /etc/nginx/conf.d/allowed.methods.conf + # Block POST/PUT/DELETE if own authentication is not provided. + set $combined_ha_rm "$http_authorization$request_method"; + if ($combined_ha_rm = POST) { + return 405 "POST method is not allowed"; + } + if ($combined_ha_rm = PUT) { + return 405 "PUT method is not allowed"; + } + if ($combined_ha_rm = DELETE) { + return 405 "DELETE method is not allowed"; + } + + if ($http_authorization != "") { + # override with own authentication if provided + set $finalAuth $http_authorization; + } + + # allow to upload big layers + client_max_body_size 0; + + # only cache GET requests + proxy_cache_methods GET; +EOF else cat << 'EOF' > /etc/nginx/conf.d/allowed.methods.conf # Block POST/PUT/DELETE. Don't use this proxy for pushing. @@ -163,8 +195,8 @@ fi # normally use non-debug version of nginx NGINX_BIN="/usr/sbin/nginx" -if [[ "a${DEBUG}" == "atrue" ]]; then - if [[ ! -f /usr/bin/mitmweb ]]; then +if [ "${DEBUG}" = "true" ]; then + if [ ! -f /usr/bin/mitmweb ]; then echo "To debug, you need the -debug version of this image, eg: :latest-debug" exit 3 fi @@ -182,8 +214,8 @@ if [[ "a${DEBUG}" == "atrue" ]]; then echo "Access mitmweb via http://127.0.0.1:8081/ " fi -if [[ "a${DEBUG_HUB}" == "atrue" ]]; then - if [[ ! -f /usr/bin/mitmweb ]]; then +if [ "${DEBUG_HUB}" = "true" ]; then + if [ ! -f /usr/bin/mitmweb ]; then echo "To debug, you need the -debug version of this image, eg: :latest-debug" exit 3 fi @@ -205,8 +237,8 @@ if [[ "a${DEBUG_HUB}" == "atrue" ]]; then echo "Access mitmweb for outgoing DockerHub requests via http://127.0.0.1:8082/ " fi -if [[ "a${DEBUG_NGINX}" == "atrue" ]]; then - if [[ ! -f /usr/sbin/nginx-debug ]]; then +if [ "${DEBUG_NGINX}" = "true" ]; then + if [ ! -f /usr/sbin/nginx-debug ]; then echo "To debug, you need the -debug version of this image, eg: :latest-debug" exit 4 fi @@ -219,8 +251,8 @@ fi # Timeout configurations -echo "" > /etc/nginx/nginx.timeouts.config.conf -cat <>/etc/nginx/nginx.timeouts.config.conf +echo -n "" > /etc/nginx/nginx.timeouts.config.conf +cat <>/etc/nginx/nginx.timeouts.config.conf # Timeouts # ngx_http_core_module @@ -238,23 +270,23 @@ cat <>/etc/nginx/nginx.timeouts.config.conf proxy_connect_read_timeout ${PROXY_CONNECT_READ_TIMEOUT}; proxy_connect_connect_timeout ${PROXY_CONNECT_CONNECT_TIMEOUT}; proxy_connect_send_timeout ${PROXY_CONNECT_SEND_TIMEOUT}; -EOD +EOF echo -e "\nTimeout configs: ---" cat /etc/nginx/nginx.timeouts.config.conf echo -e "---\n" # Upstream SSL verification. -echo "" > /etc/nginx/docker.verify.ssl.conf -if [[ "a${VERIFY_SSL}" == "atrue" ]]; then - cat << EOD > /etc/nginx/docker.verify.ssl.conf +echo -n "" > /etc/nginx/docker.verify.ssl.conf +if [ "${VERIFY_SSL}" = "true" ]; then + cat < /etc/nginx/docker.verify.ssl.conf # We actually wanna be secure and avoid mitm attacks. # Fitting, since this whole thing is a mitm... # We'll accept any cert signed by a CA trusted by Mozilla (ca-certificates-bundle in alpine) proxy_ssl_verify on; proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; proxy_ssl_verify_depth 2; -EOD +EOF echo "Upstream SSL certificate verification enabled." else echo "Upstream SSL certificate verification is DISABLED." diff --git a/nginx.conf b/nginx.conf index 897628e..054b187 100644 --- a/nginx.conf +++ b/nginx.conf @@ -245,6 +245,7 @@ echo "Docker configured with HTTPS_PROXY=$scheme://$http_host/" proxy_ignore_headers X-Accel-Expires Expires Cache-Control Set-Cookie; # Add the authentication info, if the map matched the target domain. + include "/etc/nginx/conf.d/allowed_override_auth.conf"; proxy_set_header Authorization $finalAuth; # Use SNI during the TLS handshake with the upstream.