Kubernetes has gotten a lot of attention lately for making application deployment much easier. In order to deploy an application on Kubernetes, we must create Docker images for the various services. This is not aways a trivial task. When it comes to WordPress, our job is much easier because there is an official WordPress Docker image that we can use. But that image doesn’t handle HTTPS. Not only we would like HTTPS, but we want it to be automatic, meaning that the certificates should renew without manual intervention. Can this be done?

In order to get HTTPS into a Docker image, we must first acquire a certificate, which used to be a lot of hassle. Now we have Letsencrypt that can generate the certificates automatically and also can renew them automatically. We will show how Letsencrypt can be installed into the official WordPress Docker image, and how HTTPS can be enabled using the certificates it generates. 

How do you know this works? You are reading this blog from an SSL enabled Wordpress container.

Why is HTTPS important for your blog?

Two things can happen to you if you are not using HTTPS:

Lack of privacy

Someone can eavesdrop on communication with your site and read everything you say. You may think this is not a big deal, since a blog is public information anyway, but this rule also applies to logging into your dashboard. When you type in your password, it can be read by a “man-in-the-middle”, and your site can get compromised. When someone signs up with your blog and registers their account, their passwords can get stolen, too.

The impostor problem

Someone can pretend to be you. HTTPS makes sure you are talking to the intended person, and your readers and customers are talking to the real you.

As a result Google takes SSL very seriously and actually ranks HTTP sites lower, than those who only serve content through HTTPS.

Two step process

The difference between Dockerized services and standard Linux services

An important fact to know about Docker containers is that they follow a different set of best practices to how a “normal” Linux box would be built. On a Linux box we use utilities such as service and systemctl to start and stop services which were installed some time earlier, perhaps when the machine was initially provisioned.

With a Docker container, all configuration is typically done upfront. We don’t normally start and stop the main service of the Docker container. Instead, we kill the container, rebuild the image with the new configuration, and run that new image.

Letsencrypt’s requirements

Letsencrypt expects us to start with an HTTP-based container, run Certbot on it, then point the Apache (WordPress) config files to the keys it generated, and then restart Apache to make the config changes take effect.

There’s clearly a contradiction between these two approaches!

We will do this as a two step process, with two Docker images

The official WordPress Docker image works exactly as described above. The web server in it, Apache, cannot be restarted, because it is loaded in a Docker style, with the Dockerfile CMD command. The container will exit if Apache is killed, since that is its main running process. When I tried to make it just reload its config files, instead of restarting it, that also crashed the WordPress Docker image. How will we install the SSL keys if the web server cannot be restarted to read the new configuration. Apache, the web server that WordPress is using, won’t read its configuration files unless it’s restarted, once we get our certs, we must find a way to cycle Apache, one way or another.

In the Docker world, we must kill the entire container, rebuild the new container with the config files pointing to the certificates, and then start this new container up in the place of the old one.

This is why we use a two step process.

We will create two Docker files: The first one Dockerfile.init, will be used for creating an initial container without SSL. We will only deploy this container onto Kubernetes once, in order to generate the first certificates using Letsencrypt’s certbot-auto –standalone tool, and save them on persistent external storage. Then we will throw this container away, and generate an image from the second Docker file, Dockerfile, which will point to our certificates generated in step one. From this point on getting updated certificates should be easy, by running certbot-auto renew from cron daily.

Dockerfile.init

Get full file.

Let’s start by building the first Docker image. This will really only need Certbot in it. Certbot is a tool that can automatically generate certicifcates for you using the Letsencrypt system, if and only if, you run it on the box where you need the certficates. This is because it needs to verify that you control that box. By running on the the box the domain is pointing to, you will have proven to the folks at Letsencrypt that you deserve a certificate for that domain. The beauty of all of this is that you only need to run one command certbot-auto –standalone, and everything will be taken care of for you to create the certficates. Then we will manually install them into Apache,, which WordPress runs on.

Note that there is an Apache plugin for Certbot, but we don’t want to include Apache in this first container, so we will use standalone mode. This way Certbot will run its own tiny web server and we will have to copy the certs where we want them, but that is OK. We have a script for that!

We will base this image off of a small Debian image that works pretty well, overall. We add curl so that we can download packages.

FROM debian:stretch-slim

RUN apt-get update && \
    apt-get -y install curl

Let’s add certbot-auto, which is the tool from the Letsencrypt project that will automatically generate the certificates. This is the only thing that we actually really need in this container. That is why we used an extremely simple container for this first step that has nothing but certbot-auto in it. Letsencrypt has an official dockerized certbot. But then we might run into surprises when we deploy that into our Kubernetes cluster. We used the same base here that the WordPress image uses to minimize surprises. We’d rather sort compatibility issues (e.g. file system issues) out once, not twice for two different image types.

RUN curl https://dl.eff.org/certbot-auto -O && \
    mv certbot-auto /usr/local/bin/certbot-auto && \
    chown root /usr/local/bin/certbot-auto && \
    chmod 0755 /usr/local/bin/certbot-auto

We created a tiny script that will call certbot-auto –standalone to generate the certficates, because it requires a bunch of settings and if you don’t supply them at the command line, it will ask a lot of questions along the way. That’s not good in a Dockerfile. So let’s first look at this little script get-initial-cert.sh:

Get full file.

#!/usr/bin/env bash

/usr/local/bin/certbot-auto certonly --standalone \
    --manual-public-ip-logging-ok \
    -m admin@${WP_SITE_DOMAIN_NAME} --email admin@${WP_SITE_DOMAIN_NAME} --agree-tos --noninteractive \
    --domain ${WP_SITE_DOMAIN_NAME} --rsa-key-size 2048 && \
    cp -fr /etc/letsencrypt /var/www/html && \
    ln -s ${PERSISTENT_MOUNT_POINT}/letsencrypt/live/${WP_SITE_DOMAIN_NAME} ${PERSISTENT_MOUNT_POINT}/certs && \
    find ${PERSISTENT_MOUNT_POINT} && \
    echo "get-initial-cert.sh ran at `date`" >> ${PERSISTENT_MOUNT_POINT}/get-initial-cert.log

The main parameter that this script sets up is the domain name. Certbot needs to know what domain you claim to own and want certs for. The other important parameter is the mount point of the persistent disk where we want to store the certs so the second container can find them. We also create a symbolic link so that the second container won’t have to reference the certificates from Letsencrypt’s directory structure, because that uses domain names in the path. We want the second container to simply be able to point to the certs without including the domain name, for simplicity. Finally the final find statement is only there because it will list the files created by Certbot recursively, so that we can see them in the logs for diagnostic purposes.

Now we must add the script to Dockerfile.init. We made certbot the CMD for the Dockerfile, so all the container will do is run this command, then it will exit.

ADD src/get-initial-cert.sh /usr/local/bin/get-initial-cert.sh
RUN chown root /usr/local/bin/get-initial-cert.sh && \
    chmod 0755 /usr/local/bin/get-initial-cert.sh
CMD ["/usr/local/bin/get-initial-cert.sh"]

This other handy little script will build the container and copy it onto Dockerhub. This can be pushed into a public repo.

#!/usr/bin/env bash

docker build -f Dockerfile.init -t kornel/wordpress-init .
docker push kornel/wordpress-init

Now, all you have to do is deploy onto Kubernetes. I’m using Kubernetes engine from Google, but I assume other clouds will be quite similar. Notice how this is just a job, not a pod or deployment, because it will run and exit. It sets up the environment variables, opens the HTTP port for certbot –standalone, and hooks up the disk to save the certs on. Here’s the deployment script my-site.yam:

apiVersion: batch/v1
kind: Job
metadata:
  name: wordpress-demo
..
      containers:
      - name: wordpress-demo
        image: kornel/wordpress-init
        env:
        - name: WP_SITE_DOMAIN_NAME
        ..
        ports:
        - containerPort: 80
        ..
        volumeMounts:
        - name: wordpress-demo-vol
        ..
      volumes:
        ..

Now you need to use this script to deploy /wordpress-init onto your live cluster, type kubectl get pods -w to wait for the container id to show up. At this point kubectl logs -f <container id> will show you the logs. Then monitor the logs to see if it succeeded.

Get full file.

kubectl -f my-site.yaml
get pods -w 
..wait a little..
NAME                           READY     STATUS    RESTARTS   AGE
wordpress-demo-fbffd4875-s8h5s 1/1       Running   0          11h

kubectl logs wordpress-demo-fbffd4875-s8h5s

The get-initial-cert.sh script will take care of everything for you with regards to getting the Letsencrypt certificates. They will be copied into the location /etc/letsencrypt/live/<youtdomain>, symlinked into certs, and listed out by find so that you can see all of this in the logs. Once the script successfully completes, we have the certificates on our persistent disk and we are ready to load up the second SSL capable WordPress container. We can throw Dockerfile.init away at this point.

Dockerfile – for the “real” container

Get full file.

Now, we need to install the the keys from the previous container into Apache in the second container to enable SSL. They are on the persistent disk that we attached to the former container. We would ideally have the keys now, when we are building the second container, because this is the time we are configuring Apache. Unfortunately, they won’t be accessible until we run the image with the disk attached.

Chicken egg problem

We need the keys to build the image and we need the image to get the keys off our persistent disk. Luckily, we can temporarily fool Apache and configure it with empty certificate files, at the time we build the image. Then when we run the image at deployment time, we mount the disk to the same path so that the empty certs will be over-written by the real ones, and by the time Apache gets configured, it will find the keys.

ADD src/empty.pem /var/www/html/certs/fullchain.pem
ADD src/empty.pem /var/www/html/certs/privkey.pem
ADD src/ssl-site.conf /etc/apache2/sites-available/ssl-site.conf
RUN a2enmod ssl && a2ensite ssl-site

Where ssl-site.conf has these lines:

SSLCertificateFile /var/www/html/certs/fullchain.pem
SSLCertificateKeyFile /var/www/html/certs/privkey.pem

Here’s the full Apache configuration file:

This Dockefile will add an additional tool to renew the certificates, renew-cert.sh.

Get full file.

#!/usr/bin/env bash

cp -fr /var/www/html/letsencrypt /etc && \
    certbot-auto renew && \
    cp -fr /etc/letsencrypt /var/www/html && \
    echo "Ran renew `date`" >> /var/log/renew.log

We must copy this into our Dockerfile:

ADD src/renew-cert.sh /usr/local/bin/renew-cert.sh
RUN chown root /usr/local/bin/renew-cert.sh && \
    chmod 0755 /usr/local/bin/renew-cert.sh

We are going to run the renew script from cron, so we must set it up by creating a crontab file first:

0 22 * * * /usr/local/bin/renew-cert.sh

Second, we must add and configure crontab. We’ll need cron for the automatic renewal later.

ADD src/crontab-renew.txt /tmp/crontab-renew.txt
RUN apt-get -y install cron && crontab -u root /tmp/crontab-renew.txt

Finally, we build our second container and push it to a repo. There is nothing private in this containers, since our keys are on the persistent disk. This container only has empty place holders, so we can push it into a public repository.

#!/usr/bin/env bash

docker build -t kornel/wordpress .
docker push kornel/wordpress

In the Kubernetes deployment descriptor your must change your image name to <yourname>/wordpress. Also make sure that both ports are exposed. You will need 443 for HTTPS.

containers:
        - image: <yourname>/wordpress-https
          name: wordpress
          ports:
            - containerPort: 80
              name: http
            - containerPort: 443
              name: https

Swap out the deployment from the previous image with this new one:

Get full file.

kubectl delete deployment -l app=wordpress
kubectl create -f my-https-site.yaml

Verify that it works

Before telling WordPress that it should switch to HTTPS, you should verify that you have SSL working correctly on your blog. Once you fully switch your site to HTTPS, if SSL is not set up correctly, you can lock yourself out of your site. I have personally done this!

The SSL analysis tool https://www.ssllabs.com will come into your site, scan your SSL configuration, and it will give you a detailed report of how good of a job you’ve done setting everything up. If it all checks out, you are ready to change the WordPress configuration to always use secure URLs.

Tell WordPress to switch to HTTPS

Now, when the container comes up it will be all configured for HTTPS. But that is only the container. Once WordPress comes up, you will need to tell WordPress also that it is running on a “box” with HTTPS capability. I used Really Simple SSL to configure WordPress for HTTPS which worked great You can do it by hand, but using a plugin like that really makes life simpler. Then you can use Better Search Replace, another great plugin, to fix all your HTTP references if you’re upgrading an existing WordPress database.

Conclusion

Aren’t you happy that you’ve done this? Now you have an SSL-enabled WordPress container, deployed onto Kubernetes Engine, that will automatically renew its certificates. Google should eventually start ranking your site higher, once it sees that you have better security. Therefore, this upgrade should pay you plenty of dividends monetizing your site going forward.

If you are stuck trying to implement this for your site (or if you find errors), feel free to ask questions in the comments section below. I will respond to any questions or email me directly at info@synkre.com.