Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor frontend container #759

Merged
merged 35 commits into from
Oct 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
07f99b0
Merge remote-tracking branch 'origin/master' into refactor_frontend_c…
jmgrady Oct 7, 2020
c3bac42
Split frontend into nginx and certbot containers
jmgrady Oct 9, 2020
be638e4
Split frontend into nginx and certbot containers
jmgrady Oct 9, 2020
5b1841f
Create templates directory for nginx
jmgrady Oct 11, 2020
9c7003b
Fix SSL cert location for QA hosts
jmgrady Oct 11, 2020
6478c15
Fix duplicate port definition
jmgrady Oct 11, 2020
8da54c3
Fix build context for certmgr
jmgrady Oct 11, 2020
8a9fcb4
Merge remote-tracking branch 'origin/master' into refactor_frontend_c…
jmgrady Oct 11, 2020
f3efe92
Set self-signed key size to 4096 bits
jmgrady Oct 11, 2020
b536d9a
Fix default self-signed cert expiration
jmgrady Oct 11, 2020
c939102
Add CERT_NAME environment variable
jmgrady Oct 12, 2020
ba9c842
Move functions for self-signed certs to selfsigned.sh
jmgrady Oct 12, 2020
2896156
Prevent false error message
jmgrady Oct 12, 2020
0330130
Add default variables for combine_config role
jmgrady Oct 12, 2020
2994baa
Fix compound if statement
jmgrady Oct 12, 2020
6d0d3da
Update docker_setup.py for certmgr files
jmgrady Oct 12, 2020
9fe5255
Update environment variables for server
jmgrady Oct 12, 2020
856f3d9
Refactor to use symbolic link to select self-signed or letsencrypt cert.
jmgrady Oct 14, 2020
7ed0280
Store certbot cert in its own volume
jmgrady Oct 16, 2020
b737ca9
Fix Python lint issues
jmgrady Oct 16, 2020
564dc38
Fix test argument in selfsigned.sh
jmgrady Oct 16, 2020
9a793a8
Update environment variable definitions for new design
jmgrady Oct 16, 2020
629ca8d
Add superfluous comma to keep lint happy
jmgrady Oct 16, 2020
7e81541
Remove CERT_CREATE_ONLY variable
jmgrady Oct 16, 2020
ef84a95
Update documentation for frontend refactoring
jmgrady Oct 16, 2020
8d5b13a
Merge remote-tracking branch 'origin/master' into refactor_frontend_c…
jmgrady Oct 16, 2020
d7cda32
Update comments, fix variable name
jmgrady Oct 16, 2020
56f67b7
Fix environment variable for Nginx certificate location
jmgrady Oct 16, 2020
c29d77b
Documentation update
jmgrady Oct 16, 2020
7479fa8
Merge remote-tracking branch 'origin/master' into refactor_frontend_c…
jmgrady Oct 16, 2020
ee1a3cf
Merge branch 'master' into refactor_frontend_container
johnthagen Oct 16, 2020
bd3a5cf
Changes from review comments.
jmgrady Oct 16, 2020
e318a5b
Merge remote-tracking branch 'origin/master' into refactor_frontend_c…
jmgrady Oct 16, 2020
b0cf09a
Combine docker environment file specifications
jmgrady Oct 16, 2020
66e4674
Update nginx image to use 1.19
jmgrady Oct 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@

# Generated Docker files for development use
docker-compose.yml
.env.backend
.env.frontend
.env.*
.env*.local
nginx/scripts

Expand Down
10 changes: 4 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ COPY . ./
RUN npm run build

# Production environment.
FROM staticfloat/nginx-certbot
FROM nginx:1.19

RUN mkdir /etc/nginx/templates

WORKDIR /app

ENV NGINX_HOST_DIR /usr/share/nginx/html

COPY --from=builder /app/build ${NGINX_HOST_DIR}

# Copy default self-signed certificate.
# Overwrite this with real certificate for authentication in production.
COPY nginx/certs /ssl

COPY nginx/conf.d/thecombine.conf /etc/nginx/user.conf.d/thecombine.conf
COPY nginx/templates/* /etc/nginx/templates/
10 changes: 10 additions & 0 deletions certmgr/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM certbot/certbot:v1.8.0

RUN apk update && \
apk upgrade && \
apk add --no-cache bash && \
apk add --no-cache curl

COPY scripts/*.sh /usr/bin/

ENTRYPOINT ["entrypoint.sh"]
102 changes: 102 additions & 0 deletions certmgr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Design notes for the certbot container for TheCombine

## Certificate storage

The certificates are stored in a docker volume. The volume is named letsencrypt
and is mounted by the `frontend` and the `certmgr` containers. Both containers mount
the volume at `/etc/letsencrypt`.

At the top level, there are 3 directories in `/etc/letsencrypt`:
- `letsencrypt` - stores the certificates created by letsencrypt and managed by `certbot`.
`/etc/letsencrypt` is created as a symbolic link to `/etc/letsencrypt/letsencrypt`
- `nginx` - The Nginx web server is configured to read its SSL certificates from:
- SSL Certificate: `/etc/letsencrypt/nginx/<server_name>/fullchain.pem`
- SSL Private Key: `/etc/letsencrypt/nginx/<server_name>/privkey.pem`

This is implemented by setting up `/etc/letsencrypt/nginx/ssl/<server_name>` as a
symbolic link to the self-signed or letsencrypt certificates.
- `selfsigned` - stores a self-signed certificate for the server in
`/etc/letsencrypt/selfsigned/<server_name>`

## Entrypoint scripts for the certbot container for TheCombine

### Environment Variables

The following environment variables are used by the certmgr to control its
behavior:

| Variable Name | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| CERT_MODE | May be one of `self-signed`, `letsencrypt`, `cert-server`, or `cert-client` to specify which of the certificate management personalities to use |
| CERT_EMAIL | Set to e-mail address for certificate expiration notices first |
| CERT_STAGING | Primary domain for the certificate - used to specify location of certificate |
| CERT_DOMAINS | A space separated list of domains for the certificate. |
| CERT_VERBOSE | Set to 1 for verbose output to aid in debugging; 0 otherwise. |
| SERVER_NAME | Name of the server - used to specify the directory where the certificates are stored. |
| MAX_CONNECT_TRIES | Number of times to check if the webserver is up before attempting to get a certificate from letsencrypt. |

### selfsigned.sh
`selfsigned.sh` implements the certmgr behavior when `CERT_MODE` is set to
`self-signed`. It creates a self-signed certificate to be used by the nginx web
server and enters a loop to periodically check to see if the certificate needs
to be renewed/recreated. The certificate is valid for 10 years and will be
renewed when there are less than 10 days until is expires.

### letsencrypt.sh
`letsencrypt.sh` implements the certmgr behavior when `CERT_MODE` is set to
`letsencrypt`. If there is no certificate or if it was issued by `localhost`,
then `letsencrypt.sh` will:
- create a self-signed certificate
- wait for the webserver to come up
- use `certbot` to request a certificate from `letsencrypt` using the webroot
authentication method
- redirect the webserver configuration to point to the new certificate
Once there is a letsencrypt certificate, `letsencrypt.sh` enters a loop to check
every 12 hours if the certificate should be renewed.

### certserver.sh
*Not Implemented Yet*

`certserver.sh` will implement the certmgr behavior when `CERT_MODE` is set to
`cert-server`. It will create multiple certificates and push them to an AWS S3 bucket

### certclient.sh
*Not Implemented Yet*

`certclient.sh` will implement the certmgr behavior when `CERT_MODE` is set to
`cert-client`. It will fetch an SSL certificate from the AWS S3 bucket (when an
internet connection is available).

## Installation Requirements

The following scenarios list the various requirements for the Ansible playbook
that is used to setup the various targets, `playbook_target_setup.yml` and the
Python script that is used to setup the development environment,
`docker_setup.py`. `docker_setup.py` is only used for setting up a development
environment that uses a self-signed certificate.

When installing TheCombine for using self-signed certificates, e.g. for the QA
server or for development use, the following steps are required:
1. Build each container:
- frontend
- backend
- certbot
2. Install & configure the docker files:
1. for Linux server (e.g. QA server)
- run the Ansible script, `playbook_target_setup.yml`
- connect to the target as the user `combine`
- cd to `/opt/combine`
- run `docker-compose up --detach`
2. for Development mode,
- cd to the project root directory
- run `scripts/docker_setup.py`
- run `docker-compose build`
- run `docker-compose up --detach`

Note that the machines that are configured to get their certificates from
_Let's Encrypt_ need to have the frontend webserver reloaded in order to switch
from the self-signed certificate to the one from _Let's Encrypt_. To do this,
run:
```
docker-compose exec frontend nginx -s reload
```
20 changes: 20 additions & 0 deletions certmgr/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /bin/bash

CERT_MODE=${CERT_MODE:="self-signed"}

case ${CERT_MODE} in
self-signed)
selfsigned.sh
;;
letsencrypt)
letsencrypt.sh
;;
cert-server | cert-client)
echo "${CERT_MODE} is not implemented yet"
;;
*)
echo "${CERT_MODE} is not recognized"
;;
esac

exit 99
139 changes: 139 additions & 0 deletions certmgr/scripts/func.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#! /bin/bash

####################################################
# Initialize all the variables used by these scripts
####################################################
init_vars() {
# set variables to their default values if they are not
# specified
CERT_STORE=${CERT_STORE:="/etc/cert_store"}
# Folder for certicates as configured by the Nginx webserver
CERT_EMAIL=${CERT_EMAIL:=""}
CERT_STAGING=${CERT_STAGING:=0}
MAX_CONNECT_TRIES=${MAX_CONNECT_TRIES:=15}
if [ "${CERT_MODE}" = "self-signed" ] ; then
SELF_SIGNED_EXPIRE=3650
CERT_MIN_DAYS_TO_EXPIRE=10
else
SELF_SIGNED_EXPIRE=1
CERT_MIN_DAYS_TO_EXPIRE=1
fi
# create $cert_domains as an array of domain names
IFS=" " read -r -a cert_domains <<< "${CERT_DOMAINS}"
debug_log "Self-signed Certificates stored in ${CERT_STORE}"
debug_log "Certificate name: ${SERVER_NAME}"
debug_log "Domains: ${cert_domains[*]}"
debug_log "Certificates expire in ${SELF_SIGNED_EXPIRE}"
debug_log "Minimum days before renewal: ${CERT_MIN_DAYS_TO_EXPIRE}"

CERTBOT_DIR="/etc/letsencrypt/live/${SERVER_NAME}"
# Path where the self-signed certificates are stored
SELF_SIGNED_PATH="${CERT_STORE}/selfsigned/${SERVER_NAME}"
NGINX_CERT_PATH="${CERT_STORE}/nginx/${SERVER_NAME}"
}

init_cert_store() {
# Create nginx, and selfsigned directories
for subdir in "nginx" "selfsigned" ; do
mkdir -p "${CERT_STORE}/${subdir}"
done
}

debug_log() {
if [ "${CERT_VERBOSE}" = "1" ] ; then
echo $*
fi
}

update_link() {
src=$1
target=$2

debug_log "linking ${src} to ${target}"
link_target=`readlink ${target}`
if [ "${link_target}" != "${src}" ] ; then
if [ -L "${target}" ]; then
rm "${target}"
debug_log " Old link removed"
fi
ln -s ${src} ${target}
else
debug_log " ${target} already points to ${src}"
fi

}

create_selfsigned_cert() {
debug_log "Self-signed certificates are stored in ${SELF_SIGNED_PATH}"
mkdir -p ${SELF_SIGNED_PATH}
openssl req -x509 -nodes -newkey rsa:4096 -days ${SELF_SIGNED_EXPIRE} -keyout "${SELF_SIGNED_PATH}/privkey.pem" -out "${SELF_SIGNED_PATH}/fullchain.pem" -subj '/CN=localhost'
debug_log "Created certificate in ${CERT_STORE} for ${cert_domains}"
debug_log "Expires: "`openssl x509 -in "${SELF_SIGNED_PATH}/fullchain.pem" -noout -enddate`

# Update Nginx link
update_link "${SELF_SIGNED_PATH}" "${NGINX_CERT_PATH}"
}


renew_selfsigned_cert()
{
CERT_FILE="${SELF_SIGNED_PATH}/fullchain.pem"
debug_log "Checking for renewal of ${CERT_FILE}"

CERT_MIN_SEC_TO_EXPIRE=$(( ${CERT_MIN_DAYS_TO_EXPIRE} * 3600 * 24 ))
if [ -f ${CERT_FILE} ] ; then
openssl x509 -noout -in "${CERT_FILE}" -checkend ${CERT_MIN_SEC_TO_EXPIRE} > /dev/null
if [ "$?" = "1" ] ; then
echo "Renewing the certificate for ${cert_domains}"
create_selfsigned_cert
fi
else
echo "Restoring the certificate for ${cert_domains}"
create_selfsigned_cert
fi
}

create_certbot_cert() {
debug_log "### Requesting Let's Encrypt certificate for ${cert_domains} ..."

# Select appropriate email arg
if [ -z "${CERT_EMAIL}" ] ; then
email_arg="--register-unsafely-without-email"
else
email_arg="--email ${CERT_EMAIL}"
fi

# Enable staging mode if needed
if [ ${CERT_STAGING} != "0" ]; then staging_arg="--staging"; fi

debug_log `pwd`
cert_cmd="certbot certonly --webroot -w /var/www/certbot \
${staging_arg} \
${email_arg} \
-d ${CERT_DOMAINS} \
--rsa-key-size 4096 \
--agree-tos \
--non-interactive \
--force-renewal"
debug_log "$cert_cmd"
if $cert_cmd ; then
update_link "/etc/letsencrypt/live/${SERVER_NAME}" "${NGINX_CERT_PATH}"
fi
}

wait_for_webserver() {
debug_log "Attempting connection to ${cert_domains}"
debug_log "Max attempts = ${MAX_CONNECT_TRIES}"
count=0;
while [ ${count} -lt ${MAX_CONNECT_TRIES} ] ; do
debug_log "Connection attempt ${count}"
if curl -I "http://${cert_domains}" 2>&1 | grep -w "200\|301" ; then
debug_log "${cert_domains} is up."
return 0;
fi
let "count+=1"
sleep 10
done
echo "Failed to connect to ${cert_domains}" >2
return 1;
}
42 changes: 42 additions & 0 deletions certmgr/scripts/letsencrypt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#! /bin/bash

# Source in func.sh so we can have our nice tools
. $(cd $(dirname $0); pwd)/func.sh

# Initialize the container
init_vars
init_cert_store

# Create the initial certificate if the certs have not already
# been created
if [ ! -L "${NGINX_CERT_PATH}" ] ; then
echo "Creating initial self-signed certificate"
create_selfsigned_cert
fi

# Lookup the issuer of the certificate (either pre-existing or just created)
CERT_ISSUER=""
if [ -f "${NGINX_CERT_PATH}/fullchain.pem" ] ; then
CERT_ISSUER=`openssl x509 -in "${NGINX_CERT_PATH}/fullchain.pem" -noout -issuer | sed 's/issuer=CN *= *//'`
debug_log "Issuer for existing certificate is: ${CERT_ISSUER}"
fi

# If it is a self-signed cert, wait for the webserver to come up
# and replace it with a cert from letsencrypt
if [ -z "${CERT_ISSUER}" ] || [ "${CERT_ISSUER}" = "localhost" ] ; then
echo "Waiting for webserver to come up"
if ! wait_for_webserver ; then
debug_log "Could not connect to webserver"
#exit 1
fi

echo "Request certificate from Let's Encrypt"
create_certbot_cert
fi

# Check for certificate renewal every 12 hours
while :; do
certbot renew
sleep 12h &
wait $!
done
20 changes: 20 additions & 0 deletions certmgr/scripts/selfsigned.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /bin/bash

# Source in func.sh so we can have our nice tools
. $(cd $(dirname $0); pwd)/func.sh

# Initialize the container
init_vars
init_cert_store

# Check to see if the certificate already exists
if [ ! -f "${SELF_SIGNED_PATH}/fullchain.pem" ] ; then
echo "Creating self-signed certificate"
create_selfsigned_cert
fi

while true; do
renew_selfsigned_cert
sleep 24h &
wait $!
done
2 changes: 1 addition & 1 deletion deploy/roles/the_combine_app/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ combine_program_id: CombineBackend

#############################
# environment variables for service
combine_env_vars:
combine_backend_env_vars:
ASPNETCORE_ENVIRONMENT: Production
DOTNET_PRINT_TELEMETRY_MESSAGE: false
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Description=The Combine Backend service.
[Service]
User={{ combine_user }}
WorkingDirectory={{ combine_backend_dir }}/publish
{% for item in combine_env_vars|dict2items %}
{% for item in combine_backend_env_vars|dict2items %}
Environment="{{ item.key }}={{ item.value }}"
{% endfor %}
{% for item in combine_private_env_vars|dict2items %}
Expand Down
Loading