Skip to content

Commit

Permalink
Merge branch 'files_perm'
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrien Ferrand committed Nov 3, 2017
2 parents 39bc40e + 6536910 commit f50f957
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 67 deletions.
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ ENV CERTBOT_VERSION 0.19.0
# Let's Encrypt configuration
ENV LETSENCRYPT_STAGING false
ENV LETSENCRYPT_USER_MAIL [email protected]

# Lexicon configuration
ENV LEXICON_PROVIDER cloudflare

# Container specific configuration
ENV PFX_EXPORT false
ENV PFX_EXPORT_PASSPHRASE ""
ENV CERTS_DIRS_MODE 0750
ENV CERTS_FILES_MODE 0640
ENV CERTS_USER_OWNER root
ENV CERTS_GROUP_OWNER root

# Install dependencies, certbot, lexicon, prepare for first start and clean
RUN apk --no-cache --update add rsyslog git openssl libffi supervisor docker \
Expand All @@ -29,11 +36,12 @@ RUN apk --no-cache --update add rsyslog git openssl libffi supervisor docker \
COPY files/run.sh /scripts/run.sh
COPY files/watch-domains.sh /scripts/watch-domains.sh
COPY files/autorestart-containers.sh /scripts/autorestart-containers.sh
COPY files/autocmd-containers.sh /scripts/autocmd-containers.sh
COPY files/crontab /etc/crontab
COPY files/supervisord.conf /etc/supervisord.conf
COPY files/authenticator.sh /var/lib/letsencrypt/hooks/authenticator.sh
COPY files/cleanup.sh /var/lib/letsencrypt/hooks/cleanup.sh
COPY files/pfx-export-hook.sh /scripts/pfx-export-hook.sh
COPY files/deploy-hook.sh /scripts/deploy-hook.sh
COPY files/renew.sh /scripts/renew.sh

RUN chmod +x /scripts/*
Expand Down
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
* [Data persistency](#data-persistency)
* [Share certificates with the host](#share-certificates-with-the-host)
* [Share certificates with other containers](#share-certificates-with-other-containers)
* [Reconfiguration on a running container](#reconfiguration-on-a-running-container)
* [Restart containers when a certificate is renewed](#restart-containers-when-a-certificate-is-renewed)
* [Certificates files permissions](#certificates-files-permissions)
* [Runtime operations](#runtime-operations)
* [Certificates reconfiguration at runtime](#certificates-reconfiguration-at-runtime)
* [Restart containers when a certificate is renewed](#restart-containers-when-a-certificate-is-renewed)
* [Call a reload command on containers when a certificate is renewed](#call-a-reload-command-on-containers-when-a-certificate-is-renewed)
* [Miscellaneous and testing](#miscellaneous-and-testing)
* [Activate staging ACME servers](#activating-staging-acme-servers)
* [Auto-export certificates in PFX format](#auto-export-certificates-in-pfx-format)
Expand Down Expand Up @@ -168,15 +171,27 @@ docker run \

The volume `/etc/letsencrypt` will be available for the SMTP container, which can use a generated certificate for its own concern (here, securing the SMTP protocol).

## Reconfiguration on a running container
### Certificates files permissions

By default certificates files (`cert.pem`, `privkey.pem`, _etc._) are accessible only to the user/group owning `/etc/letsencrypt`, which is **root** by default. It means that generated certificates cannot be used by non-root processes (in other containers or on the host).

You can modify file mode of `/etc/letsencrypt/archive` and `/etc/letsencrypt/live` folders and their content to allow non-root processes to access the certificates. Set environment variables `CERTS_DIRS_MODE (default: 0750)` and `CERTS_FILES_MODE (default: 0640)` to modify directories and files mode respectivly. For example, a file mode of `0644` and directory mode of `0755` will open access to everyone.

Alternatively or cumulatively you may need to change the owner user/group of `/etc/letsencrypt/archive` and `/etc/letsencrypt/live` folders and their content. To do so, specify user/group name or uid/gid in the relevant environment variables: `CERTS_USER_OWNER (default: root)` and `CERTS_GROUP_OWNER (default: root)`.

_(Warning) Certificates files permissions, introduced in container version `1.4`, will modify default permissions for certificates. Previously, `/etc/letsencrypt/live` and `/etc/letsencrypt/archive` were `0750`, their sub-folders where `0755` and contained files were `0644`. Now theses folders and their sub-folders are `0750` while contained files are `0640`: this should not lead to any regression, as the parent folders were of a more restrictive permission than their content, leading certs files to be unaccessible to non-root processes. However for pathological cases you will need to set environment variable `CERTS_DIRS_MODE` and `CERTS_FILES_MODE` appropriately._

## Runtime operations

### Certificates reconfiguration at runtime

If you want to add a new certificate, remove one, or extend existing one to other domains, you just need to modify the `domains.conf` file from the host. Once saved, the container will automatically mirror the modifications in `/etc/letsencrypt` volume. If new certificates need to be generated, please note that approximately 30 seconds are required for each generation before modifications are visible.

Please check the container logs to follow the operations.

## Restart containers when a certificate is renewed
### Restart containers when a certificate is renewed

As said in introduction, most of the non-Web services require a restart when the certificate is changed. And this will occur at least once each two months. To ensure correct propagation of the new certificates in your Docker services, one special entry can be added at the end of a line for the concerned certificate in `domains.conf`.
As said in introduction, most of the non-Web services require a restart when the certificate is changed. And this will occur at least once each two months. To ensure correct propagation of the new certificates in your Docker services, one special entry can be added at the **end** of a line for the concerned certificate in `domains.conf`.

This entry takes the form of `autorestart-containers=container1,container2,container3` where `containerX` is the name of a container running on the same Docker instance than `letsencrypt-dns`.

Expand All @@ -191,14 +206,15 @@ smtp.example.com imap.example.com autorestart-containers=smtp
auth.example.com
```

Then execute following commands
Then execute following commands:

```bash
docker run \
--name letsencrypt-dns \
--volume /etc/letsencrypt/domains.conf:/etc/letsencrypt/domains.conf \
--volume /etc/letsencrypt/domains.conf:/etc/letsencrypt/domains.conf \
--volume /var/docker-data/letsencrypt:/etc/letsencrypt \
--env '[email protected]' \
--volume /var/run/docker.sock:/var/run/docker.sock \
--env '[email protected]' \
--env 'LEXICON_PROVIDER=cloudflare' \
--env 'LEXICON_CLOUDFLARE_USERNAME=my_user' \
--env 'LEXICON_CLOUDFLARE_TOKEN=my_secret_token' \
Expand All @@ -214,6 +230,24 @@ docker run \

If the certificate `smtp.example.com` is renewed, the container named `smtp` will be restarted. Renewal of `auth.example.com` will not restart anything.

### Call a reload command on containers when a certificate is renewed

Restarting a container when a certificate is renewed is sufficient for all cases. However one drawback is that the target processes will stop during a little time, and consequently the services provided are not continuous. This may be ok for non critical services, but problematic for things like authentication services or database servers.

If a target process allows it, the letsencrypt-dns container can call a reload configuration command on the target container when a certificate is renewed. In this case, service is not stopped and immediatly takes into account the new config, including the new certificate. Apache2 for example (example only, as an http challenge will be a better option here) can see its configuration to be hot-reloaded by invoking the command `apachectl graceful` in the target container.

To specify which command to launch on which container when a certificate is renewed, one will put at the **end** of the relevant line of `domains.conf` a special entry which takes the form of `autocmd-containers=container1:command1,container2:command2 arg2a arg2b,container3:command3 arg3a`. Comma `,` separates each container/command configuration, colon `:` separates the container name from the command to launch. Commands must be executable files, located in the $PATH of the target container, or accessed by their full path.

In the case of an Apache2 server embedded in a container named `my-apache` to be reloaded when certificate `web.example.com` is renewed, put following entry in `domains.conf`:

```
web.example.com autocmd-containers=my-apache:apachectl graceful
```

If the certificate `web.example.com` is renewed, command `apachectl graceful` will be invoked on container `my-apache`, and the apache2 service will use the new certificate without killing any HTTP session.

_(Limitations on invokable commands) The option `autocmd-container` is intended to call a simple executable file with few potential arguments. It is not made to call some advanced bash script, and would likely fail if you do so. In fact, the command is not executed in a shell on the target, and variables will be resolved against the lets-encrypt container environment. If you want to operate advanced scripting, put an executable script in the target container, and use its path in `autocmd-container` option._

## Miscellaneous and testing

### Activating staging ACME servers
Expand Down
44 changes: 44 additions & 0 deletions files/autocmd-containers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh

domain=$1
containers_and_cmd=$2

if [ ! -S /var/run/docker.sock ]; then
echo "ERROR: /var/run/docker.sock socket is missing."
exit 1
fi

if [ ! -d /etc/letsencrypt/archive/$domain ]; then
echo "ERROR: /etc/letsencrypt/archive/$domain directory is missing."
fi

# Load hash of the certificate
current_hash=`md5sum /etc/letsencrypt/live/$domain/cert.pem | awk '{ print $1 }'`
while true; do
new_hash=`md5sum /etc/letsencrypt/live/$domain/cert.pem | awk '{ print $1 }'`

if [ "$current_hash" != "$new_hash" ]; then
# Extract container name and its command
IFS=','; for container_and_cmd in $containers_and_cmd; do
# Extract container name and its command
container_name=""
command=""
IFS=':'; for entry in $container_and_cmd; do
if [ -z $container_name ]; then
container_name="$entry"
else
command="$entry"
fi
done; unset IFS
echo ">>> Executing command '$command' for container $container_name because certificate for $domain has been modified."
# Execute it
docker exec $container_name $command
done; unset IFS

# Keep new hash version
current_hash="$new_hash"
fi

# Wait 1s for next iteration
sleep 1
done
7 changes: 3 additions & 4 deletions files/autorestart-containers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

domain=$1
containers=$2
IFS=' ,'

if [ ! -S /var/run/docker.sock ]; then
echo "ERROR: /var/run/docker.sock socket is missing."
Expand All @@ -21,14 +20,14 @@ while true; do

if [ "$current_hash" != "$new_hash" ]; then
echo ">>> Restarting dockers $containers because certificate for $domain has been modified."
for container in $containers; do
IFS=','; for container in $containers; do
docker restart $container
done
done; unset IFS

# Keep new hash version
current_hash="$new_hash"
fi

# Wait 1s for next iteration
sleep 1
done
done
16 changes: 16 additions & 0 deletions files/deploy-hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh

# Construct PFX file for new cert if needed
if [ "$PFX_EXPORT" = "true" ]; then
openssl pkcs12 -export \
-out $RENEWED_LINEAGE/cert.pfx \
-inkey $RENEWED_LINEAGE/privkey.pem \
-in $RENEWED_LINEAGE/cert.pem \
-certfile $RENEWED_LINEAGE/chain.pem \
-password pass:$PFX_EXPORT_PASSPHRASE
fi

# Synchronize mode and user/group for new certificate files
chmod $CERTS_DIRS_MODE $(find $RENEWED_LINEAGE ${RENEWED_LINEAGE/live/archive} -type d)
chmod $CERTS_FILES_MODE $(find $RENEWED_LINEAGE ${RENEWED_LINEAGE/live/archive} -type f)
chown -R $CERTS_USER_OWNER:$CERTS_GROUP_OWNER $RENEWED_LINEAGE ${RENEWED_LINEAGE/live/archive}
8 changes: 0 additions & 8 deletions files/pfx-export-hook.sh

This file was deleted.

8 changes: 1 addition & 7 deletions files/renew.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
#!/bin/sh

echo "Launch renew test"

hooks=""
if [ "$PFX_EXPORT" = "true" ]; then
hooks="$hooks --deploy-hook pfx-export-hook.sh"
fi

certbot renew -n $hooks
certbot renew -n --deploy-hook deploy-hook.sh
27 changes: 21 additions & 6 deletions files/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@
# Ensure domain.conf exists
touch /etc/letsencrypt/domains.conf

# Load crontab and incrontab
# Ensure certs folders exist, and with correct permissions
mkdir -p /etc/letsencrypt/live /etc/letsencrypt/archive
if [ "$CERTS_DIR_WORLD_READABLE" = "true" ]; then
chmod 0755 /etc/letsencrypt/live /etc/letsencrypt/archive
elif [ "$CERTS_DIR_GROUP_READABLE" = "true" ]; then
chmod 0750 /etc/letsencrypt/live /etc/letsencrypt/archive
else
chmod 0700 /etc/letsencrypt/live /etc/letsencrypt/archive
fi

# Synchronize certs files mode and user/group permissions
chmod $CERTS_DIRS_MODE $(find /etc/letsencrypt/live /etc/letsencrypt/archive -type d)
chmod $CERTS_FILES_MODE $(find /etc/letsencrypt/live /etc/letsencrypt/archive -type f)
chown -R $CERTS_USER_OWNER:$CERTS_GROUP_OWNER /etc/letsencrypt/live /etc/letsencrypt/archive

# Load crontab
crontab /etc/crontab

# Update TLDs
Expand All @@ -14,13 +29,13 @@ if [ "$PFX_EXPORT" = "true" ]; then
for i in `ls /etc/letsencrypt/live`
do
openssl pkcs12 -export \
-out "/etc/letsencrypt/live/$i/cert.pfx" \
-inkey "/etc/letsencrypt/live/$i/privkey.pem" \
-in "/etc/letsencrypt/live/$i/cert.pem" \
-certfile "/etc/letsencrypt/live/$i/chain.pem" \
-out /etc/letsencrypt/live/$i/cert.pfx \
-inkey /etc/letsencrypt/live/$i/privkey.pem \
-in /etc/letsencrypt/live/$i/cert.pem \
-certfile /etc/letsencrypt/live/$i/chain.pem \
-password pass:$PFX_EXPORT_PASSPHRASE
done
fi

# Start supervisord
/usr/bin/supervisord -c /etc/supervisord.conf
/usr/bin/supervisord -c /etc/supervisord.conf
65 changes: 32 additions & 33 deletions files/watch-domains.sh
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
#!/bin/sh

autorestart_pattern="autorestart-containers="

staging_cmd=""
if [ "$LETSENCRYPT_STAGING" = true ]; then
staging_cmd="--staging"
fi

deploy_hooks=""
if [ "$PFX_EXPORT" = "true" ]; then
deploy_hooks="$deploy_hooks --deploy-hook pfx-export-hook.sh"
fi

current_hash=
while true; do
# Calculate the new domains.conf file hash
new_hash=`md5sum /etc/letsencrypt/domains.conf | awk '{ print $1 }'`
if [ "$current_hash" != "$new_hash" ]; then
# Clean all autorestart containers instances
# Clean all autorestart/autocmd containers instances
rm -f /etc/supervisord.d/*_autorestart-containers
rm -f /etc/supervisord.d/*_autocmd-containers

echo "#### Registering Let's Encrypt account if needed ####"
certbot register -n --agree-tos -m $LETSENCRYPT_USER_MAIL $staging_cmd

echo "#### Creating missing certificates if needed (~1min for each) ####"
while read entry; do
while read -r entry; do
autorestart_config=`echo $entry | grep -E -o 'autorestart-containers=.*' | sed 's/autocmd-containers=.*//' | sed 's/autorestart-containers=//' | xargs`
autocmd_config=`echo $entry | grep -E -o 'autocmd-containers=.*' | sed 's/autorestart-containers=.*//' | sed 's/autocmd-containers=//' | xargs`
clean_domains=`echo $entry | sed 's/autorestart-containers=.*//' | sed 's/autocmd-containers=.*//' | xargs`
domains_cmd=""
main_domain=""
containers=""

for domain in $entry; do
if [ "${domain#*$autorestart_pattern}" != "$domain" ]; then
containers=${domain/autorestart-containers=/}
elif [ -z $main_domain ]; then
main_domain=$domain
domains_cmd="$domains_cmd -d $domain"
else
domains_cmd="$domains_cmd -d $domain"
fi
done
for domain in $clean_domains; do
if [ -z $main_domain ]; then
main_domain=$domain
fi
domains_cmd="$domains_cmd -d $domain"
done

echo ">>> Creating a certificate for domain(s):$domains_cmd"
certbot certonly \
Expand All @@ -49,18 +41,27 @@ while true; do
--manual-cleanup-hook /var/lib/letsencrypt/hooks/cleanup.sh \
--manual-public-ip-logging-ok \
--expand \
--deploy-hook deploy-hook.sh \
$staging_cmd \
$deploy_hooks \
$domains_cmd
if [ "$containers" != "" ]; then
echo ">>> Watching certificate for main domain $main_domain: containers $containers autorestarted when certificate is changed."
echo "[program:${main_domain}_autorestart-containers]" >> /etc/supervisord.d/${main_domain}_autorestart_containers
echo "command = /scripts/autorestart-containers.sh $main_domain $containers" >> /etc/supervisord.d/${main_domain}_autorestart_containers
echo "redirect_stderr = true" >> /etc/supervisord.d/${main_domain}_autorestart_containers
echo "stdout_logfile = /dev/stdout" >> /etc/supervisord.d/${main_domain}_autorestart_containers
echo "stdout_logfile_maxbytes = 0" >> /etc/supervisord.d/${main_domain}_autorestart_containers

if [ "$autorestart_config" != "" ]; then
echo ">>> Watching certificate for main domain $main_domain: containers $autorestart_config autorestarted when certificate is changed."
echo "[program:${main_domain}_autorestart-containers]" >> /etc/supervisord.d/${main_domain}_autorestart-containers
echo "command = /scripts/autorestart-containers.sh $main_domain $autorestart_config" >> /etc/supervisord.d/${main_domain}_autorestart-containers
echo "redirect_stderr = true" >> /etc/supervisord.d/${main_domain}_autorestart-containers
echo "stdout_logfile = /dev/stdout" >> /etc/supervisord.d/${main_domain}_autorestart-containers
echo "stdout_logfile_maxbytes = 0" >> /etc/supervisord.d/${main_domain}_autorestart-containers
fi

if [ "$autocmd_config" != "" ]; then
echo ">>> Watching certificate for main domain $main_domain: autocmd config $autocmd_config executed when certificate is changed."
echo "[program:${main_domain}_autocmd-containers]" >> /etc/supervisord.d/${main_domain}_autocmd-containers
echo "command = /scripts/autocmd-containers.sh $main_domain '$autocmd_config'" >> /etc/supervisord.d/${main_domain}_autocmd-containers
echo "redirect_stderr = true" >> /etc/supervisord.d/${main_domain}_autocmd-containers
echo "stdout_logfile = /dev/stdout" >> /etc/supervisord.d/${main_domain}_autocmd-containers
echo "stdout_logfile_maxbytes = 0" >> /etc/supervisord.d/${main_domain}_autocmd-containers
fi
done < /etc/letsencrypt/domains.conf

echo "### Revoke and delete certificates if needed ####"
Expand All @@ -77,9 +78,7 @@ while true; do

if [ "$remove_domain" = true ]; then
echo ">>> Removing the certificate $domain"
certbot revoke $staging_cmd --cert-path /etc/letsencrypt/live/$domain/cert.pem
certbot delete $staging_cmd --cert-name $domain
rm -rf /etc/letsencrypt/live/$domain
certbot revoke -n $staging_cmd --cert-path /etc/letsencrypt/live/$domain/cert.pem
fi
done

Expand All @@ -92,4 +91,4 @@ while true; do

# Wait 1s for next iteration
sleep 1
done
done

0 comments on commit f50f957

Please sign in to comment.