Certificate
Create a certifacate using docker
September 11, 2019

Defining the Web Server Configuration (nginx.conf)

nginx.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
server {
    listen 80;
    listen [::]:80;

    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;

    server_name subdomain.domain.com;

    location / {
            proxy_pass http://platform:8080;
    }

    location ~ /.well-known/acme-challenge {
            allow all;
            root /var/www/html;
    }
}

Creating the Docker Compose File (docker-compose.yml)

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
version: '3.8'

services:
    db:
        image: mysql:8.0.27
        volumes:
            - ./data/db:/var/lib/mysql
            - ./conf/init-scripts/mysql:/docker-entrypoint-initdb.d #All files in this directory are automatically executed in the alphabetical order on container creation. 
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: secret
            MYSQL_DATABASE: databasename
            MYSQL_USER: username
            MYSQL_PASSWORD: password

    platform:
        image: registry.gitlab.com/project-name/application-name
        depends_on:
            - db
        restart: unless-stopped

    webserver:
        image: nginx:latest
        container_name: webserver
        restart: unless-stopped
        ports:
            - "80:80"
        volumes: 
            - web-root:/var/www/html
            - ./conf/nginx-conf:/etc/nginx/conf.d
            - certbot-etc:/etc/letsencrypt
            - certbot-var:/var/lib/letsencrypt
        depends_on:
            - platform

    certbot:
        image: certbot/certbot
        container_name: certbot
        volumes:
            - certbot-etc:/etc/letsencrypt
            - certbot-var:/var/lib/letsencrypt
            - web-root:/var/www/html
        depends_on:
            - webserver
        command: certonly --webroot --webroot-path=/var/www/html --email hi@domain.com --agree-tos --no-eff-email --staging -d subdomain.domain.com

volumes:
    data:
    certbot-etc:
    certbot-var:
    web-root:

Obtaining SSL Certificates and Credentials

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ docker-compose up -d

$ docker-compose ps

$ docker-compose exec webserver ls -la /etc/letsencrypt/live
Output
total 16
drwx------ 3 root root 4096 Dec 23 16:48 .
drwxr-xr-x 9 root root 4096 Dec 23 16:48 ..
-rw-r--r-- 1 root root  740 Dec 23 16:48 README
drwxr-xr-x 2 root root 4096 Dec 23 16:48 subdomain.domain.com

Now that you know your request will be successful, you can edit the certbot service definition to remove the –staging flag.

docker-compose.yml
1
2
3
4
5
6
7
...
services:
    ...
    certbot:
        ...
        command: certonly --webroot --webroot-path=/var/www/html --email hi@domain.com --agree-tos --no-eff-email --force-renewal -d subdomain.domain.com
...
1
$ docker-compose up --force-recreate --no-deps certbot

With your certificates in place, you can move on to modifying your Nginx configuration to include SSL.

Modifying the Web Server Configuration and Service Definition

Enabling SSL in our Nginx configuration will involve adding an HTTP redirect to HTTPS and specifying our SSL certificate and key locations. It will also involve specifying our Diffie-Hellman group, which we will use for Perfect Forward Secrecy.

Since you are going to recreate the webserver service to include these additions, you can stop it now:

1
$ docker-compose stop webserver

Next, create a directory in your current project directory for your Diffie-Hellman key:

1
mkdir dhparam

Generate your key with the openssl command:

1
sudo openssl dhparam -out /home/admin/platform/conf/dhparam/dhparam-2048.pem 2048

Add the following code to the file to redirect HTTP to HTTPS and to add SSL credentials, protocols, and security headers.

nginx.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
server {
    listen 80;
    listen [::]:80;
        
    server_name subdomain.domain.com;

    location ~ /.well-known/acme-challenge {
        allow all;
        root /var/www/html;
    }

    location / {
        rewrite ^ https://$host$request_uri? permanent;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name subdomain.domain.com;
    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/subdomain.domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/subdomain.domain.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

    #ssl_buffer_size 8k;

    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    #ssl_ecdh_curve secp384r1;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # replace with the IP address of your resolver
    resolver 8.8.8.8;

    location / {
        try_files $uri @platform;
    }

    location @platform {
        proxy_pass http://platform:8080;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
        # enable strict transport security only if you understand the implications
        # HSTS (ngx_http_headers_module is required) (63072000 seconds)
        add_header Strict-Transport-Security "max-age=63072000" always;
    }

    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;
}

In the webserver service definition of docker-compose.yml file, add the following port mapping and the dhparam named volume:

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
version: '3.8'

services:
    db:
        image: mysql:8.0.27
        volumes:
            - ./data/db:/var/lib/mysql
            - ./conf/init-scripts/mysql:/docker-entrypoint-initdb.d #All files in this directory are automatically executed in the alphabetical order on container creation. 
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: secret
            MYSQL_DATABASE: databasename
            MYSQL_USER: username
            MYSQL_PASSWORD: password

    platform:
        image: registry.gitlab.com/project-name/application-name
        depends_on:
            - db
        restart: unless-stopped

    webserver:
        image: nginx:latest
        container_name: webserver
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
        volumes: 
            - web-root:/var/www/html
            - ./conf/nginx-conf:/etc/nginx/conf.d
            - certbot-etc:/etc/letsencrypt
            - certbot-var:/var/lib/letsencrypt
            - ./conf/dhparam:/etc/ssl/certs
        depends_on:
            - platform

    certbot:
        image: certbot/certbot
        container_name: certbot
        volumes:
            - certbot-etc:/etc/letsencrypt
            - certbot-var:/var/lib/letsencrypt
            - web-root:/var/www/html
        depends_on:
            - webserver
        command: certonly --webroot --webroot-path=/var/www/html --email hi@domain.com --agree-tos --no-eff-email --force-renewal -d subdomain.domain.com

volumes:
    data:
    certbot-etc:
    certbot-var:
    web-root:
    dhparam:

Recreate the webserver service:

1
$ docker-compose up -d --force-recreate --no-deps webserver

You should also see the lock icon in your browser’s security indicator. If you would like, you can navigate to the SSL Labs Server Test landing page or the Security Headers server test landing page. The configuration options we’ve included should earn your site an A rating on both.

Renewing Certificates

Let’s Encrypt certificates are valid for 90 days, so you will want to set up an automated renewal process to ensure that they do not lapse. One way to do this is to create a job with the cron scheduling utility. In this case, we will schedule a cron job using a script that will renew our certificates and reload our Nginx configuration.

Open a script called ssl_renew.sh in your project directory:

1
$ nano ssl_renew.sh

Add the following code to the script to renew your certificates and reload your web server configuration:

1
2
3
4
5
6
7
8
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/admin/platform/
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Close the file when you are finished editing. Make it executable:

1
$ chmod +x ssl_renew.sh

Next, open your root crontab file to run the renewal script at a specified interval:

1
$ sudo crontab -e 

At the bottom of the file, add the following line:

1
*/5 * * * * /home/admin/platform/ssl_renew.sh >> /var/log/cron.log 2>&1

This will set the job interval to every five minutes, so you can test whether or not your renewal request has worked as intended. We have also created a log file, cron.log, to record relevant output from the job.

After five minutes, check cron.log to see whether or not the renewal request has succeeded:

1
$ tail -f /var/log/cron.log

You should see output confirming a successful renewal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Output
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Killing webserver ... done

You can now modify the crontab file to set a daily interval. To run the script every day at noon, for example, you would modify the last line of the file to look like this:

1
0 12 * * * /home/admin/platform/ssl_renew.sh >> /var/log/cron.log 2>&1

You will also want to remove the --dry-run option from your ssl_renew.sh script:

1
2
3
4
5
6
7
8
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/admin/platform/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Your cron job will ensure that your Let’s Encrypt certificates don’t lapse by renewing them when they are eligible. You can also set up log rotation with the Logrotate utility to rotate and compress your log files.

Errors

nginx: [emerg] cannot load certificate “/etc/letsencrypt/live/subdomain.domain.com/fullchain.pem”: BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen(’/etc/letsencrypt/live/subdomain.domain.com/fullchain.pem’,‘r’) error:2006D080:BIO routines:BIO_new_file:no such file)