Created
January 1, 2020 23:14
-
-
Save shakefu/23b77484dff9f01cc55d4b144b12d750 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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="[email protected]" | |
# 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