Skip to content

Instantly share code, notes, and snippets.

@shakefu
Created January 1, 2020 23:14
Show Gist options
  • Save shakefu/23b77484dff9f01cc55d4b144b12d750 to your computer and use it in GitHub Desktop.
Save shakefu/23b77484dff9f01cc55d4b144b12d750 to your computer and use it in GitHub Desktop.
# Docker Service Deployment
# Please complete the following Coding Exercise as part of the Interview Process.
# - You should proceed with this exercise as if you were building this for
# deployment to production.
# - It is expected that you will have to do some research in order to get this
# done.
# - When you have completed this exercise, please email your recruiting contact
# or the hiring manager, so they can end the exercise and review.
# - You can use whatever languages you like. You should have good reasoning
# backing up your decisions.
# - For convenience purposes, submit your answer as a single Dockerfile right
# here in CoderPad. You may have to generate files using bash commands here.
# You will be testing your Dockerfile on your personal computer, as you will
# not be able to build and test Dockerfiles in this interface. Coderpad, in
# this case, is just our method for submission of solutions.
# We want to create a simple HTTPS web service that takes the following request:
# > curl -k https://webservice:port/health
# and returns this result:
# > {'status': 'ok'}
# We want to package this as a Docker image, so you should submit a Dockerfile.
# - Generate a new self-signed SSL certificate during each Docker build.
# - Take an environment variable named PORT and listen for HTTPS traffic on that port.
# - Send request logs to stdout, including at least client IP, HTTP method,
# URL, response time, and User-Agent.
###############################################################################
# # Shenanigans with certificates and nginx and Docker
#
# This is a fun Dockerfile which creates a simple healthcheck endpoint that has
# SSL termination. For extra difficulty and to keep things interesting for
# msyelf, the final product has as-minimal-as-possible attack surface by
# deriving from busybox, with only the essential binaries and dynamic
# libraries. This makes it 13.8Mb smaller than the official nginx alpine base
# image.
#
# Commentary is extra verbose to explain what exactly is happening and why
# where applicable.
#
#
# ## Installation
#
# The only requirement Docker 18 or greater (and this Dockerfile).
#
#
# ## Building
#
# Any of the build args starting with `CERT_` may be changed to suit your
# needs.
#
# `docker build -t realreal/health .`
# Builds with default CA and certificate name settings.
#
# `docker build -t realreal/health --build-arg CERT_PUBLIC_CN="localhost" .`
# Builds with certificate name set to "localhost" instead of "*.local".
#
# `docker build -t realreal/health --no-cache .`
# Force the creation of a new SSL certificate by breaking the image cache.
# Otherwise, unless you modify any of the `CERT_*` build arguments, the image
# will cache and reuse the same certificates for ease of testing and use.
#
# ## Running
#
# `docker run -p 443:443 realreal/health`
# Run with defaults on HTTPS/443.
#
# `docker run -p 443:443 -p 80:80 realreal/health`
# Run with defaults on HTTPS/443 with 301 redirect from HTTP/80.
#
# `PORT=8080 docker run -d -e "PORT=$PORT" -p "$PORT:$PORT" realreal/health`
# Run with custom port 8080 for HTTPS.
#
# `docker run realreal/health 'cat /etc/nginx/ssl/ca.pem'`
# Output the self-signed root CA chain for trusting locally.
#
#
# ## Recommended testing
#
# Build the image:
# 1. `docker build -t realreal/health .`
#
# Export the Self-Signed CA and import it for trust (OS X):
# 2. `docker run --rm realreal/health 'cat /etc/nginx/ssl/ca.pem' > selfsigned_ca.pem`
# 3. `open selfsigned_ca.pem`
# 4. In the "login" keychain find the "Self-Signed CA" certificate and set it
# to "Always Trust".
#
# Validate the image works...
# Starting the container
# 5. `docker run --rm -p 443:443 realreal/health`
# With curl (note you need `-k` if you didn't trust the CA first.
# 6. `curl https://localhost/health`
# And in a browser:
# 7. `open https://localhost/health`
#
# Validate the PORT env var works...
# 8. `docker run --rm -p 8080:8080 -e PORT=8080 realreal/health`
# 9. `curl https://localhost:8080/health`
###############################
# Stage 1: Certificate creation
#
# This stage could have easily been just an `openssl x509` command, but it was
# more fun to create an entire CA certificate chain that can be trusted by a
# browser, so I found a base image to use which does just that.
# https://github.com/gitphill/openssl-alpine
FROM pgarrett/openssl-alpine AS certs
# Override the build args if you want to change these defaults for the
# certificate creation.
# This is actually the country, just named weird in the base image
ARG CERT_COUNTY="US"
ARG CERT_STATE="CA"
ARG CERT_LOCATION="San Francisco"
ARG CERT_ORGANISATION="The RealReal, Inc."
ARG CERT_ROOT_CN="Self-Signed CA"
ARG CERT_ISSUER_CN="The RealReal, Inc."
ARG CERT_PUBLIC_CN="localhost"
ARG CERT_RSA_KEY_NUMBITS=4096
ARG CERT_DAYS=9001
# Now we have to map them to environment variables so our script picks them up
# since it's not intended to be run at build time. This alone would be a pretty
# good argument for forking the source repo to customize a bit better
# This is actually the country
ENV COUNTY=$CERT_COUNTY \
STATE=$CERT_STATE \
LOCATION=$CERT_LOCATION \
ORGANISATION=$CERT_ORGANISATION \
ROOT_CN=$CERT_ROOT_CN \
ISSUER_CN=$CERT_ISSUER_CN \
PUBLIC_CN=$CERT_PUBLIC_CN \
ROOT_NAME="ca" \
ISSUER_NAME="issuer" \
PUBLIC_NAME="selfsigned" \
RSA_KEY_NUMBITS=$CERT_RSA_KEY_NUMBITS \
DAYS=$CERT_DAYS
# Create certificates
RUN ./docker-entrypoint.sh &&\
# Move certs from the VOLUME so they don't get overwritten
mkdir -p /etc/nginx/ssl &&\
cp /etc/ssl/certs/* /etc/nginx/ssl
##############################
# Stage 2: nginx configuration
#
# This stage is really all that you would need for the most simple form of this
# Dockerfile. A call to `openssl req -newkey` would generate the cert and key
# to be used by the nginx configuration below.
#
# In fact, if you `--target nginx` when building this Dockerfile, you can run
# the image without the final stripped down stage, if you override the default
# CMD with `nginx`:
# `docker build -t realreal/health --target nginx .`
# `docker run --rm -it -p 443:443 -p 80:80 realreal/health nginx`
#
# This stage provides the musl-built binaries and dynamic libraries we need to
# pull into the final stripped image.
# https://github.com/nginxinc/docker-nginx/
FROM nginx:stable-alpine AS nginx
# Clean out default nginx conf
RUN rm -r /etc/nginx/*
# Collect the certs from our creation stage, and merge to nginx conf dir
COPY --from=certs /etc/nginx/ssl /etc/nginx/ssl
# Normally this configuration would be in an external file and just COPY'd in.
# Instead we use this fun syntax because Dockerfiles don't support heredocs
# yet, and the RUN command will smush it all together if you don't include the
# newlines.
#
# There's a lot of settings and tuning that nginx would really want if this was
# serving production content and not just a healthcheck.
#
# This could also be generated in the next stage, but I like that it makes the
# nginx config and SSL import a one line COPY for fewer final layers.
RUN echo $'\
daemon off;\n\
user nobody;\n\
events {}\n\
http {\n\
# Don't give away our version to attackers
server_tokens off;\n\
server {\n\
listen 443 ssl default_server;\n\
server_name _;\n\
ssl_certificate /etc/nginx/ssl/selfsigned.crt;\n\
ssl_certificate_key /etc/nginx/ssl/selfsigned.key;\n\
location /health {\n\
add_header Content-Type application/json;\n\
# The spec called for single quotes in this response, but double quotes
# means it will actually validate as JSON
return 200 \'{\"status\": \"ok\"}\';\n\
}\n\
location / {\n\
return 404;\n\
}\n\
}\n\
server {\n\
listen 80;\n\
server_name _;\n\
return 301 https://$host:443$request_uri;\n\
}\n\
}'\
> /etc/nginx/nginx.conf
# " ... this commented out quote is just here to fix vim syntax highlighting,
# which didn't like some of the quote escaping above.
###############################
# Stage 3: Stripped final image
#
# Create a super minimal attack surface by ditching everything we possibly can
# and still have nginx run.
#
# A better approach would be to statically compile nginx so no dyanamic
# libraries are needed, but this is already needlessly complex, so we're going
# to skip that.
#
# https://github.com/docker-library/busybox
FROM busybox:musl
# Take responsibility for my actions
LABEL maintainer="shakefu@gmail.com"
# Import nginx required libs
COPY --from=nginx /lib /lib
COPY --from=nginx /usr/lib/libpcre* /lib/
# Import nginx binary
COPY --from=nginx /usr/sbin/nginx /usr/sbin/nginx
# Import nginx config
COPY --from=nginx /etc/nginx /etc/nginx
RUN \
# No users or groups in this container
echo -e "nobody:x:65534:" > /etc/group &&\
echo -e "nobody:x:65534:65534:nobody:/:/bin/false" > /etc/passwd &&\
# nginx requires these directories exist to run
mkdir -p /var/run &&\
mkdir -p /var/log/nginx &&\
mkdir -p /var/cache/nginx &&\
# Set up nginx logging to stdout/stderr
ln -sf /dev/stdout /var/log/nginx/access.log &&\
ln -sf /dev/stderr /var/log/nginx/error.log
# Provide a sensible default port
ENV PORT=443
# This lets us provide other commands to the container easily, e.g. for
# exporting the ca.pem
ENTRYPOINT ["sh", "-c"]
# Combined with the entrypoint this lets us override the nginx config SSL port
# since that was part of the spec. We just use sed for this, since really the
# port should be mapped at the container networking level, not inside it.
CMD ["sed s/443/$PORT/ -i.orig /etc/nginx/nginx.conf && /usr/sbin/nginx"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment