├── .github ├── FUNDING.yml ├── dependabot.yml ├── version_extractor.sh └── workflows │ ├── build_latest.yml │ ├── build_tags.yml │ └── unit_tests.yml ├── LICENSE ├── README.md ├── docs ├── advanced_usage.md ├── certbot_authenticators.md ├── changelog.md ├── dockerhub_tags.md ├── good_to_know.md └── nginx_tips.md ├── examples ├── docker-compose.yml ├── example_server.conf ├── example_server_multicert.conf ├── example_server_overrides.conf └── nginx-certbot.env ├── src ├── Dockerfile ├── Dockerfile-alpine ├── Makefile ├── nginx_conf.d │ └── redirector.conf ├── requirements.txt └── scripts │ ├── create_dhparams.sh │ ├── run_certbot.sh │ ├── run_local_ca.sh │ ├── start_nginx_certbot.sh │ └── util.sh └── tests ├── fixtures ├── ipv4_addresses.txt ├── ipv6_addresses.txt ├── nginx_config │ ├── multi_files │ │ ├── 10-example.org.conf │ │ ├── 20-anew.example.org.conf │ │ ├── 30-example.com.conf │ │ ├── 40-example.net_1.conf │ │ └── 40-example.net_2.conf │ └── single_files │ │ ├── multi_certbot_domain_directive.conf │ │ ├── multi_server_multi_cert_multi_name.conf │ │ ├── multi_server_single_cert_single_name.conf │ │ ├── regex_server_names.conf │ │ ├── single_certbot_domain_directive.conf │ │ ├── single_server_multi_cert_single_name.conf │ │ ├── single_server_single_cert_multi_name.conf │ │ └── single_server_single_cert_single_name.conf └── not_ip_addresses.txt └── util.bats /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JonasAlfredsson 2 | patreon: jonasal 3 | ko_fi: jonasal 4 | custom: [ "buymeacoffee.com/jonasal", "paypal.me/JonasAlfredsson" ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for Dockerfiles. 4 | - package-ecosystem: "docker" 5 | directory: "/src" 6 | schedule: 7 | interval: "daily" 8 | time: "04:00" 9 | open-pull-requests-limit: 10 10 | # Maintain dependencies for pip packages. 11 | - package-ecosystem: "pip" 12 | directory: "/src" 13 | schedule: 14 | interval: "daily" 15 | open-pull-requests-limit: 10 16 | # Maintain dependencies for GitHub Actions. 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | open-pull-requests-limit: 10 22 | -------------------------------------------------------------------------------- /.github/version_extractor.sh: -------------------------------------------------------------------------------- 1 | # This is a small helper script used to extract and verify the tag that is to 2 | # be set on the Docker container. This file expects that the GitHub Action 3 | # variable GITHUB_REF is passed in as the one and only argument. 4 | 5 | if [ -z "${1}" ]; then 6 | >&2 echo "Input argument was empty" 7 | exit 1 8 | fi 9 | 10 | app_major=$(echo ${1} | sed -n -r -e 's&^refs/.+/v([1-9])\.[0-9]+\.[0-9]+.*$&\1&p') 11 | app_minor=$(echo ${1} | sed -n -r -e 's&^refs/.+/v[1-9]\.([0-9]+)\.[0-9]+.*$&\1&p') 12 | app_patch=$(echo ${1} | sed -n -r -e 's&^refs/.+/v[1-9]\.[0-9]+\.([0-9]+).*$&\1&p') 13 | nginx_version=$(echo ${1} | sed -n -r -e 's&^refs/.+/.*-nginx([1-9]\.[0-9]+\.[0-9]+)$&\1&p') 14 | 15 | if [ -n "${app_major}" -a -n "${app_minor}" -a -n "${app_patch}" -a -n "${nginx_version}" ]; then 16 | echo "APP_MAJOR=${app_major}" 17 | echo "APP_MINOR=${app_minor}" 18 | echo "APP_PATCH=${app_patch}" 19 | echo "NGINX_VERSION=${nginx_version}" 20 | else 21 | >&2 echo "Received the following input argument: '${1}'" 22 | >&2 echo "Could not extract all expected values: v${app_major}.${app_minor}.${app_patch}-${nginx_version}" 23 | exit 1 24 | fi 25 | -------------------------------------------------------------------------------- /.github/workflows/build_latest.yml: -------------------------------------------------------------------------------- 1 | name: "build-latest" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | - "master" 9 | paths: 10 | - "src/**" 11 | pull_request: 12 | branches: 13 | - "main" 14 | - "master" 15 | paths: 16 | - "src/Dockerfile*" 17 | - "src/requirements.txt" 18 | 19 | jobs: 20 | docker_buildx_debian: 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 210 23 | steps: 24 | - name: Run Docker on tmpfs 25 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 26 | with: 27 | tmpfs_size: 7 28 | swap_size: 9 29 | 30 | - name: Perform setup steps 31 | uses: JonasAlfredsson/checkout-qemu-buildx@v2 32 | with: 33 | should_login: ${{ github.event_name != 'pull_request' }} 34 | username: ${{ secrets.DOCKERHUB_USERNAME }} 35 | password: ${{ secrets.DOCKERHUB_TOKEN }} 36 | 37 | - name: Build and push latest Debian image 38 | uses: docker/build-push-action@v6.18.0 39 | with: 40 | context: ./src 41 | file: ./src/Dockerfile 42 | platforms: | 43 | linux/amd64 44 | linux/386 45 | linux/arm64 46 | linux/arm/v7 47 | push: ${{ github.event_name != 'pull_request' }} 48 | tags: jonasal/nginx-certbot:latest 49 | 50 | docker_buildx_alpine: 51 | runs-on: ubuntu-latest 52 | timeout-minutes: 100 53 | steps: 54 | - name: Run Docker on tmpfs 55 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 56 | with: 57 | tmpfs_size: 7 58 | swap_size: 9 59 | 60 | - name: Perform setup steps 61 | uses: JonasAlfredsson/checkout-qemu-buildx@v2 62 | with: 63 | should_login: ${{ github.event_name != 'pull_request' }} 64 | username: ${{ secrets.DOCKERHUB_USERNAME }} 65 | password: ${{ secrets.DOCKERHUB_TOKEN }} 66 | 67 | - name: Build and push latest Alpine image 68 | uses: docker/build-push-action@v6.18.0 69 | with: 70 | context: ./src 71 | file: ./src/Dockerfile-alpine 72 | platforms: | 73 | linux/amd64 74 | linux/arm64 75 | push: ${{ github.event_name != 'pull_request' }} 76 | tags: jonasal/nginx-certbot:latest-alpine 77 | -------------------------------------------------------------------------------- /.github/workflows/build_tags.yml: -------------------------------------------------------------------------------- 1 | name: "build-tags" 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "**" 7 | tags: 8 | - "v[1-9].[0-9]+.[0-9]+-nginx[1-9].[0-9]+.[0-9]+" 9 | 10 | jobs: 11 | docker_buildx_debian: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 210 14 | steps: 15 | - name: Run Docker on tmpfs 16 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 17 | with: 18 | tmpfs_size: 7 19 | swap_size: 9 20 | 21 | - name: Perform setup steps 22 | uses: JonasAlfredsson/checkout-qemu-buildx@v2 23 | with: 24 | should_login: ${{ github.event_name != 'pull_request' }} 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | 28 | - name: Extract version numbers from GitHub reference 29 | id: tagger 30 | run: bash .github/version_extractor.sh ${GITHUB_REF} >> $GITHUB_ENV 31 | 32 | - name: Build and push all Debian images 33 | uses: docker/build-push-action@v6.18.0 34 | with: 35 | context: ./src 36 | file: ./src/Dockerfile 37 | platforms: | 38 | linux/amd64 39 | linux/386 40 | linux/arm64 41 | linux/arm/v7 42 | pull: true 43 | no-cache: true 44 | push: true 45 | tags: | 46 | jonasal/nginx-certbot:${{ env.APP_MAJOR }} 47 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}.${{ env.APP_MINOR }} 48 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}.${{ env.APP_MINOR }}.${{ env.APP_PATCH }} 49 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}.${{ env.APP_MINOR }}.${{ env.APP_PATCH }}-nginx${{ env.NGINX_VERSION }} 50 | 51 | docker_buildx_alpine: 52 | runs-on: ubuntu-latest 53 | timeout-minutes: 100 54 | steps: 55 | - name: Run Docker on tmpfs 56 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 57 | with: 58 | tmpfs_size: 7 59 | swap_size: 9 60 | 61 | - name: Perform setup steps 62 | uses: JonasAlfredsson/checkout-qemu-buildx@v2 63 | with: 64 | should_login: ${{ github.event_name != 'pull_request' }} 65 | username: ${{ secrets.DOCKERHUB_USERNAME }} 66 | password: ${{ secrets.DOCKERHUB_TOKEN }} 67 | 68 | - name: Extract version numbers from GitHub reference 69 | id: tagger 70 | run: bash .github/version_extractor.sh ${GITHUB_REF} >> $GITHUB_ENV 71 | 72 | - name: Build and push all Alpine images 73 | uses: docker/build-push-action@v6.18.0 74 | with: 75 | context: ./src 76 | file: ./src/Dockerfile-alpine 77 | platforms: | 78 | linux/amd64 79 | linux/arm64 80 | pull: true 81 | no-cache: true 82 | push: true 83 | tags: | 84 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}-alpine 85 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}.${{ env.APP_MINOR }}-alpine 86 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}.${{ env.APP_MINOR }}.${{ env.APP_PATCH }}-alpine 87 | jonasal/nginx-certbot:${{ env.APP_MAJOR }}.${{ env.APP_MINOR }}.${{ env.APP_PATCH }}-nginx${{ env.NGINX_VERSION }}-alpine 88 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: "unit-tests" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | - "master" 9 | paths: 10 | - "src/scripts/**" 11 | - "tests/**" 12 | pull_request: 13 | branches: 14 | - "main" 15 | - "master" 16 | paths: 17 | - "src/scripts/**" 18 | - "tests/**" 19 | 20 | jobs: 21 | bats: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout the repository 25 | uses: actions/checkout@v4.2.2 26 | 27 | - name: Run BATS tests 28 | uses: ffurrer2/bats-action@v1.1.0 29 | with: 30 | args: ./tests 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Henri Dwyer 4 | Copyright (c) 2017 Elliot Saba 5 | Copyright (c) 2018 Jonas Alfredsson 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-nginx-certbot 2 | 3 | Automatically create and renew website SSL certificates using the 4 | [Let's Encrypt][1] free certificate authority and its client [*certbot*][2]. 5 | Built on top of the [official Nginx Docker images][9] (both Debian and Alpine), 6 | and uses OpenSSL/LibreSSL to automatically create the Diffie-Hellman parameters 7 | used during the initial handshake of some ciphers. 8 | 9 | > :information_source: The very first time this container is started it might 10 | take a long time before it is ready to respond to requests. Read more 11 | about this in the 12 | [Diffie-Hellman parameters](./docs/good_to_know.md#diffie-hellman-parameters) 13 | section. 14 | 15 | > :information_source: Please use a [specific tag](./docs/dockerhub_tags.md) 16 | when doing a Docker pull, since `:latest` might not always be 100% stable. 17 | 18 | ### Noteworthy Features 19 | - Handles multiple server names when [requesting certificates](./docs/good_to_know.md#how-the-script-add-domain-names-to-certificate-requests) (i.e. both `example.com` and `www.example.com`). 20 | - Handles wildcard domain request in case you use [DNS authentication](./docs/certbot_authenticators.md). 21 | - Can request both [RSA and ECDSA](./docs/good_to_know.md#ecdsa-and-rsa-certificates) certificates ([at the same time](./docs/advanced_usage.md#multi-certificate-setup)). 22 | - Will create [Diffie-Hellman parameters](./docs/good_to_know.md#diffie-hellman-parameters) if they are defined. 23 | - Uses the [parent container][9]'s [`/docker-entrypoint.d/`][7] folder. 24 | - Will report correct [exit code][6] when stopped/killed/failed. 25 | - You can do a live reload of configs by [sending in a `SIGHUP`](./docs/advanced_usage.md#manualforce-renewal) signal (no container restart needed). 26 | - Possibility to use this image **offline** with the help of a [local CA](./docs/advanced_usage.md#local-ca). 27 | - Both [Debian and Alpine](./docs/dockerhub_tags.md) images built for [multiple architectures][14]. 28 | 29 | 30 | 31 | # Acknowledgments and Thanks 32 | 33 | This container requests SSL certificates from [Let's Encrypt][1], with the help 34 | of their [*certbot*][2] script, which they provide for the absolutely bargain 35 | price of free! If you like what they do, please [donate][3]. 36 | 37 | This repository was originally forked from [`@henridwyer`][4] by 38 | [`@staticfloat`][5], before it was forked again by me. However, the changes to 39 | the code has since become so significant that this has now been detached as its 40 | own independent repository (while still retaining all the history). Migration 41 | instructions, from `@staticfloat`'s image, can be found 42 | [here](./docs/good_to_know.md#help-migrating-from-staticfloats-image). 43 | 44 | 45 | 46 | # Usage 47 | 48 | ## Before You Start 49 | 1. This guide expects you to already own a domain which points at the correct 50 | IP address, and that you have both port `80` and `443` correctly forwarded 51 | if you are behind NAT. Otherwise I recommend [DuckDNS][12] as a Dynamic DNS 52 | provider, and then either search on how to port forward on your router or 53 | maybe find it [here][13]. 54 | 55 | 2. I suggest you read at least the first two sections in the 56 | [Good to Know](./docs/good_to_know.md) documentation, since this will give 57 | you some important tips on how to create a basic server config, and how to 58 | use the Let's Encrypt staging servers in order to not get rate limited. 59 | 60 | 3. I don't think it is necessary to mention if you managed to find this 61 | repository, but you will need to have [Docker][11] installed for this to 62 | function. 63 | 64 | 65 | ## Available Environment Variables 66 | 67 | ### Required 68 | - `CERTBOT_EMAIL`: Your e-mail address. Used by Let's Encrypt to contact you in case of security issues. 69 | 70 | ### Optional 71 | - `DHPARAM_SIZE`: The size of the [Diffie-Hellman parameters](./docs/good_to_know.md#diffie-hellman-parameters) (default: `2048`) 72 | - `ELLIPTIC_CURVE`: The size/[curve][15] of the ECDSA keys (default: `secp256r1`) 73 | - `RENEWAL_INTERVAL`: Time interval between certbot's [renewal checks](./docs/good_to_know.md#renewal-check-interval) (default: `8d`) 74 | - `RSA_KEY_SIZE`: The size of the RSA encryption keys (default: `2048`) 75 | - `STAGING`: Set to `1` to use Let's Encrypt's [staging servers](./docs/good_to_know.md#initial-testing) (default: `0`) 76 | - `USE_ECDSA`: Set to `0` to have certbot use [RSA instead of ECDSA](./docs/good_to_know.md#ecdsa-and-rsa-certificates) (default: `1`) 77 | 78 | ### Advanced 79 | - `CERTBOT_AUTHENTICATOR`: The [authenticator plugin](./docs/certbot_authenticators.md) to use when responding to challenges (default: `webroot`) 80 | - `CERTBOT_DNS_PROPAGATION_SECONDS`: The number of seconds to wait for the DNS challenge to [propagate](./docs/certbot_authenticators.md#troubleshooting-tips) (default: certbot's default) 81 | - `CERTBOT_DNS_CREDENTIALS_DIR`: Directory where credentials for [DNS authenticators](./docs/certbot_authenticators.md#preparing-the-container-for-dns-01-challenges) should be located (default: `/etc/letsencrypt`). 82 | - `DEBUG`: Set to `1` to enable debug messages and use the [`nginx-debug`][10] binary (default: `0`) 83 | - `USE_LOCAL_CA`: Set to `1` to enable the use of a [local certificate authority](./docs/advanced_usage.md#local-ca) (default: `0`) 84 | 85 | 86 | ## Volumes 87 | - `/etc/letsencrypt`: Stores the obtained certificates and the Diffie-Hellman parameters 88 | 89 | 90 | ## Run with `docker run` 91 | Create your own [`user_conf.d/`](./docs/good_to_know.md#the-user_confd-folder) 92 | folder and place all of you custom server config files in there. When done you 93 | can just start the container with the following command 94 | ([available tags](./docs/dockerhub_tags.md)): 95 | 96 | ```bash 97 | docker run -it -p 80:80 -p 443:443 \ 98 | --env CERTBOT_EMAIL=your@email.org \ 99 | -v $(pwd)/nginx_secrets:/etc/letsencrypt \ 100 | -v $(pwd)/user_conf.d:/etc/nginx/user_conf.d:ro \ 101 | --name nginx-certbot jonasal/nginx-certbot:latest 102 | ``` 103 | 104 | > You should be able to detach from the container by holding `Ctrl` and pressing 105 | `p` + `q` after each other. 106 | 107 | As was mentioned in the introduction; the very first time this container is 108 | started it might take a long time before before it is ready to 109 | [respond to requests](./docs/good_to_know.md#diffie-hellman-parameters), please 110 | be a little bit patient. If you change any of the config files after the 111 | container is ready, you can just 112 | [send in a `SIGHUP`](./docs/advanced_usage.md#manualforce-renewal) to tell 113 | the scripts and Nginx to reload everything. 114 | 115 | ```bash 116 | docker kill --signal=HUP 117 | ``` 118 | 119 | 120 | ## Run with `docker-compose` 121 | An example of a [`docker-compose.yaml`](./examples/docker-compose.yml) file can 122 | be found in the [`examples/`](./examples) folder. The default parameters that 123 | are found inside the [`nginx-certbot.env`](./examples/nginx-certbot.env) file 124 | will be overwritten by any environment variables you set inside the `.yaml` 125 | file. 126 | 127 | > NOTE: You can use both `environment:` and `env_file:` together or only one 128 | of them, the only requirement is that `CERTBOT_EMAIL` is defined 129 | somewhere. 130 | 131 | Like in the example above, you just need to place your custom server configs 132 | inside your [`user_conf.d/`](./docs/good_to_know.md#the-user_confd-folder) 133 | folder beforehand. Then you start it all with the following command. 134 | 135 | ```bash 136 | docker-compose up 137 | ``` 138 | 139 | 140 | ## Build It Yourself 141 | This option is for if you make your own `Dockerfile`. Check out which tags that 142 | are available in [this document](./docs/dockerhub_tags.md), or on 143 | [Docker Hub][8], and then choose how specific you want to be. 144 | 145 | In this case it is possible to completely skip the 146 | [`user_conf.d/`](./docs/good_to_know.md#the-user_confd-folder) folder and just 147 | write your files directly into Nginx's `conf.d/` folder. This way you can 148 | replace the files I have built [into the image](./src/nginx_conf.d) with your 149 | own. However, if you do that please take a moment to understand what they do, 150 | and what you need to include in order for certbot to continue working. 151 | 152 | ```Dockerfile 153 | FROM jonasal/nginx-certbot:latest 154 | COPY conf.d/* /etc/nginx/conf.d/ 155 | ``` 156 | 157 | 158 | 159 | # Tests 160 | We make use of [BATS][16] to test parts of this codebase. The easiest way to 161 | run all the tests is to execute the following command in the root of this 162 | repository: 163 | 164 | ```bash 165 | docker run -it --rm -v "$(pwd):/workdir" ffurrer/bats:latest ./tests 166 | ``` 167 | 168 | 169 | 170 | # More Resources 171 | Here is a collection of links to other resources that provide useful 172 | information. 173 | 174 | - [Good to Know](./docs/good_to_know.md) 175 | - A lot of good to know stuff about this image and the features it provides. 176 | - [Changelog](./docs/changelog.md) 177 | - List of all the tagged versions of this repository, as well as bullet points to what has changed between the releases. 178 | - [DockerHub Tags](./docs/dockerhub_tags.md) 179 | - All the tags available from Docker Hub. 180 | - [Advanced Usage](./docs/advanced_usage.md) 181 | - Information about the more advanced features this image provides. 182 | - [Certbot Authenticators](./docs/certbot_authenticators.md) 183 | - Information on the different authenticators that are available in this image. 184 | - [Nginx Tips](./docs/nginx_tips.md) 185 | - Some interesting tips on how Nginx can be configured. 186 | 187 | 188 | 189 | # External Guides 190 | Here is a list of projects that use this image in various creative ways. Take 191 | a look and see if one of these helps or inspires you to do something similar: 192 | 193 | - [A `Node.js` application served over HTTPS in AWS Elastic Beanstalk](https://efraim-rodrigues.medium.com/using-docker-to-containerize-your-node-js-aefcd1ecd37d) 194 | - [Host your own `Nakama` server](https://www.snopekgames.com/tutorial/2021/how-host-nakama-server-10mo) 195 | 196 | 197 | 198 | 199 | 200 | [1]: https://letsencrypt.org/ 201 | [2]: https://github.com/certbot/certbot 202 | [3]: https://letsencrypt.org/donate/ 203 | [4]: https://github.com/henridwyer/docker-letsencrypt-cron 204 | [5]: https://github.com/staticfloat/docker-nginx-certbot 205 | [6]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/43dde6ec24f399fe49729b28ba4892665e3d7078 206 | [7]: https://github.com/nginxinc/docker-nginx/tree/master/entrypoint 207 | [8]: https://hub.docker.com/r/jonasal/nginx-certbot 208 | [9]: https://github.com/nginxinc/docker-nginx 209 | [10]: https://github.com/docker-library/docs/tree/master/nginx#running-nginx-in-debug-mode 210 | [11]: https://docs.docker.com/engine/install/ 211 | [12]: https://www.duckdns.org/ 212 | [13]: https://portforward.com/router.htm 213 | [14]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/28 214 | [15]: https://security.stackexchange.com/a/104991 215 | [16]: https://github.com/bats-core/bats-core 216 | -------------------------------------------------------------------------------- /docs/advanced_usage.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | This document contains information about features that are deemed "advanced", 4 | and will most likely require that you read some of the actual code to fully 5 | understand what is happening. 6 | 7 | ## Signal handling 8 | 9 | The container configures handlers for the following signals: 10 | 11 | - `SIGINT`, `SIGQUIT`, `SIGTERM` - Shutdown the child processes (nginx and the 12 | [sleep timer](./good_to_know.md#renewal-check-interval)) and exit the 13 | container. 14 | - `SIGHUP` - Rerun [`run_certbot.sh`](../src/scripts/run_certbot.sh) and tell 15 | nginx to test and reload the configuration files (i.e. `nginx -t` followed 16 | by `nginx -s reload`). See [Manual/Force Renewal](#manualforce-renewal), 17 | [Controlling NGINX][20], and [Changing configuration][21] for more details. 18 | - `SIGUSR1` - Tell nginx to reopen log files (i.e. `nginx -s reopen`). See 19 | [Controlling NGINX][20] and [Rotating Log-files][22] for more details. 20 | 21 | Signals can be sent to the container by using the `docker kill` command (or 22 | `docker compose kill` when using Docker Compose). For example, to send a 23 | `SIGHUP` signal to the container run: 24 | 25 | ```bash 26 | docker kill --signal=HUP 27 | ``` 28 | 29 | ## Manual/Force Renewal 30 | It might be of interest to manually trigger a renewal of the certificates, and 31 | that is why the [`run_certbot.sh`](../src/scripts/run_certbot.sh) script is 32 | possible to run standalone at any time from within the container. 33 | 34 | However, the preferred way of requesting a reload of all the configuration files 35 | is to send in a [`SIGHUP`][1] to the container: 36 | 37 | ```bash 38 | docker kill --signal=HUP 39 | ``` 40 | 41 | This will terminate the [sleep timer](./good_to_know.md#renewal-check-interval) 42 | and make the renewal loop start again from the beginning, which includes a lot 43 | of other checks than just the certificates. 44 | 45 | While this will be enough in the majority of the cases, it might sometimes be 46 | necessary to **force** a renewal of the certificates even though certbot thinks 47 | it could keep them for a while longer (like when [this][2] happened). It is 48 | therefore possible to add "force" as an argument, when calling the 49 | [`run_certbot.sh`](../src/scripts/run_certbot.sh) script, to have it append 50 | the `--force-renewal` flag to the requests made. 51 | 52 | ```bash 53 | docker exec -it /scripts/run_certbot.sh force 54 | ``` 55 | 56 | This will request new certificates regardless of when they are set to expire. 57 | 58 | > :warning: Using "force" will make new requests for **all** you certificates, 59 | so don't run it too often since there are some limits to requesting 60 | [production certificates][3]. 61 | 62 | 63 | ## Override `server_name` 64 | Nginx allows you to to do a lot of stuff in the [`server_name` declaration][18], 65 | but since the scripts inside this image compose certificate requests from the 66 | same lines we are severely limited in what is possible to define on those lines. 67 | For example the line `server_name mail.*` would produce a certificate request 68 | for the domain name `mail.*`, which is not valid. 69 | 70 | However, to combat this limitation it is possible to define a special comment 71 | on the same line in order to override what the scripts will pick up. So in this 72 | contrived example 73 | 74 | ```bash 75 | server { 76 | listen 443 ssl; 77 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 78 | 79 | server_name yourdomain.org; 80 | server_name www.yourdomain.org; # certbot_domain:*.yourdomain.org 81 | server_name sub.yourdomain.org; # certbot_domain:*.yourdomain.org 82 | server_name mail.*; # certbot_domain:*.yourdomain.org 83 | server_name ~^(?.+)\.yourdomain\.org$; 84 | ... 85 | } 86 | ``` 87 | 88 | we will end up with a certificate request which looks like this: 89 | 90 | ``` 91 | certbot --cert-name "test-name" ... -d yourdomain.org -d *.yourdomain.org 92 | ``` 93 | 94 | The fist server name will be picked up as usual, while the following three will 95 | be shadowed by the domain in the comment, i.e. `*.yourdomain.org` (and duplicate 96 | names will be removed in the final request). 97 | 98 | The last server name is special, in that it is a regex and those always start 99 | with a `~`. Since we know we will never be able to create a valid request from 100 | a name which start with that character they will always be ignored by the 101 | script (a trailing comment will take precedence instead of being ignored). 102 | A more detailed example of this can be viewed in 103 | [`example_server_overrides.conf`](../examples/example_server_overrides.conf). 104 | 105 | Important to remember is that here we define a wildcard domain name (the `*` 106 | in the the `*.yourdomain.org`), and that requires you to use an authenticator 107 | capable of DNS-01 challenges, and more info about that may be found in the 108 | [certbot_authenticators.md](./certbot_authenticators.md) document. 109 | 110 | 111 | ## Multi-Certificate Setup 112 | This is a continuation of the 113 | [RSA and ECDSA](./good_to_know.md#ecdsa-and-rsa-certificates) section from 114 | the [Good to Know](./good_to_know.md) document, where it was briefly mentioned 115 | that it is actually possible to have Nginx serve both of these certificate 116 | types at the same time, thus expanding support for semi-old devices again while 117 | also allowing the most up to date encryption to be used. [The setup][4] is a 118 | bit more complicated, but the 119 | [`example_server_multicert.conf`](../examples/example_server_multicert.conf) 120 | file should be configured so you should only have to edit the "yourdomain.org" 121 | statements at the top. 122 | 123 | How this works is that Nginx is able to [load multiple certificate files][5] 124 | for each server block, and you then configure the cipher suites in an order 125 | that prefers ECDSA certificates. The [scripts](../src/scripts/run_certbot.sh) 126 | running inside the container then looks for some (case insensitive) variant of 127 | these strings in the 128 | [`--cert-name`](./good_to_know.md#how-the-script-add-domain-names-to-certificate-requests) 129 | argument: 130 | 131 | - `-rsa` 132 | - `.rsa` 133 | - `-ecc` 134 | - `.ecc` 135 | - `-ecdsa` 136 | - `.ecdsa` 137 | 138 | and makes a certificate request with the correct type set. See the 139 | [actual commit][6] for more details, but what you need to know is that if 140 | these options are found they override the [`USE_ECDSA`](../README.md#optional) 141 | environment variable. 142 | 143 | 144 | ## Use Custom ACME URL 145 | There are two variables available at the top of the 146 | [`run_certbot.sh`](../src/scripts/run_certbot.sh) script: 147 | 148 | - `CERTBOT_PRODUCTION_URL` 149 | - `CERTBOT_STAGING_URL` 150 | 151 | which are used to define which server certbot will try to contact when 152 | requesting new certificates. These variables have default values, but it is 153 | possible to override them by defining environment vairables with the same name. 154 | This then enables you to redirect certbot to another custom URL if you, for 155 | example, are running your own custom ACME server. 156 | 157 | 158 | ## Local CA 159 | During the development phase of a website you might be testing stuff on a 160 | computer that either does not have a DNS record pointing to itself or perhaps 161 | it does not have internet access at all. Since certbot has both of these as 162 | requirements to function properly it was previously impossible to use this 163 | image during those particular situations. 164 | 165 | That is why the [`run_local_ca.sh`](../src/scripts/run_local_ca.sh) script was 166 | created, since this makes it possible to use a 167 | [local (self-signed) certificate authority][10] that can issue website 168 | certificates without relying on any external service or internet connection. 169 | It also enables us to issue certificates that are valid for `localhost` and/or 170 | IP addresses like `::1`, which are otherwise [impossible][7] for certbot to 171 | create. 172 | 173 | > You can also use this solution if your intend to deploy behind a CDN and you 174 | > do not need a "real" certificate in order to encrypt the communication 175 | > between the origin server and the CDN's infrastructure. But please read 176 | > this entire section before going forward with that kind of setup. 177 | 178 | To enable the usage of this local CA you just set 179 | [`USE_LOCAL_CA=1`](../README.md#advanced), and this will then trigger the 180 | execution of the [`run_local_ca.sh`](../src/scripts/run_local_ca.sh) script 181 | instead of the [`run_certbot.sh`](../src/scripts/run_certbot.sh) one when it is 182 | time to renew the certificates. This script, when run, will always overwrite 183 | any previous keys and certificates, so alternating between the use of a local 184 | CA and certbot without first emptying the `/etc/letsencrypt` folder is **not** 185 | supported. 186 | 187 | The script is designed to mimic certbot as closely as reasonable, so the 188 | keys/certs created are placed in the same locations as certbot would have. This 189 | means that you only have to edit the `server_name` in your server configuration 190 | files to include the variant that you want for your local instance (e.g. 191 | `localhost`) and you should be all set. 192 | 193 | However, if you navigate to your site at this point you will run into an error 194 | named similar to Firefox's `SEC_ERROR_UNKNOWN_ISSUER`, which just means that 195 | your browser does not recognize the CA that has signed your site's certificate. 196 | This is expected (since we just created our own local CA), but at this point 197 | the connection is using all the fancy HTTPS stuff, and is thus "secure", so you 198 | can just ignore this warning if you want. 199 | 200 | Another solution is to [import][9] the local CA's certificate created by this 201 | script into your browser, thus making this a known certificate authority and 202 | any of its signed certs trusted. What this file is, and how to obtain it, is 203 | explained further in the [next section](#files-and-folders). 204 | 205 | 206 | ### Files and Folders 207 | For the local CA to operate a couple of things are needed: 208 | 209 | - `caPrivkey.pem`: The private key used by the CA -> this is the most secret 210 | thing that (in a real CA's case) must be protected at all 211 | costs. 212 | - `caCert.pem`: The public certificate part of the CA -> needed by all clients 213 | in order to trust any other certificates this CA signs. 214 | - `serial.txt`: A long random hexadecimal number that is incremented by one 215 | every time a new certificate is signed by the CA. 216 | - `index.txt`: Keeps a [record][8] of all certificates that have been issued 217 | by this CA. 218 | - `new_certs/`: Folder where a copy of all the newly signed certificates are 219 | placed. 220 | 221 | All of these are created automatically by the script inside the folder defined 222 | by [`LOCAL_CA_DIR`](../src/scripts/run_local_ca.sh) (which defaults to 223 | `/etc/local_ca`), so by host mounting this folder you will be able to see all 224 | these files. By then taking the `caCert.pem` and [importing][9] it in your 225 | browser you will be able to visit these sites without the error stating that 226 | the certificate is signed by an unknown authority. 227 | 228 | An important thing to know is that these files are only created if they do 229 | not exist. What this enables is an even more advanced usecase where you might 230 | already have a private key and certificate that you trust on your devices, and 231 | you would like to continue using it for the websites you host as well. Read 232 | more about this in the [next section](#creating-a-custom-ca). 233 | 234 | 235 | ### Creating a Custom CA 236 | The validity period for the automatically created CA is only 30 days by 237 | default, and the reason for this is to deter people from using this solution 238 | blindly in production. While it is possible to quickly change this through the 239 | `LOCAL_CA_ROOT_CERT_VALIDITY` variable, there are security concerns regarding 240 | managing your own certificate authority which should be thought through before 241 | doing this in a production environment. 242 | 243 | Nevertheless, as was mentioned in the previous section it is possible to supply 244 | the [`run_local_ca.sh`](../src/scripts/run_local_ca.sh) script with a local 245 | certificate authority that has been created manually by you that you want to 246 | trust on multiple other devices. 247 | Basically all you need to do is to host mount your custom private key and 248 | certificate to the `LOCAL_CA_DIR`, and the script will use these instead of 249 | the short lived automatically created ones. Just make sure the files are named 250 | in accordance to [what the script expects](#files-and-folders), and if any one 251 | of these components are missing they will be created the first time the service 252 | is started. 253 | 254 | > As of now a password protected private key is not supported. 255 | 256 | I did not find it trivial to create a **well configured** CA, so if you want 257 | to go this route I really suggest that you read up on what you are doing and 258 | making sure all settings are correctly tuned for your usecase. 259 | 260 | There is a [lot][11] of [high-level][12] information [available][13] in regards 261 | to how to create your own CA, but what I found most confusing was exactly what 262 | was expected to be inside the [`openssl.cnf`][14] file that is necessary to 263 | have when running most of the OpenSSL commands. The configuration that is 264 | present inside the [`run_local_ca.sh`](../src/scripts/run_local_ca.sh) script 265 | should be quite minimalistic for what we need, while still providing the 266 | [strict settings][15] that some clients need else they will reject these 267 | custom certificates. 268 | 269 | The most comprehensive guide I have found is the [OpenSSL Cookbook][17], 270 | which goes into great detail about basically everything OpenSSL is able to do, 271 | along with [this post][16] which summarizes the settings needed for different 272 | certificate types. With these two you should be able to make an informed 273 | configuration in case you want to create your own custom certificate authority, 274 | and you may of course take a look at the commands used in the 275 | [`generate_ca()`](../src/scripts/run_local_ca.sh) function to help you on your 276 | way of creating your own files. 277 | 278 | When the long term CA is in place you can probably tune the `RENEWAL_INTERVAL` 279 | variable to equal something slightly less than the hard-coded `90d` expiry 280 | time of the leaf certificates created. This is the expiry time recommended by 281 | Let's Encrypt (and perhaps [soon mandated][19]), and since the renewal script 282 | _should_ always be successful you only need to run it just before the expiry 283 | time. You can, of course, run it more often than that, but it will not yield 284 | and special benefits. 285 | 286 | 287 | 288 | 289 | 290 | 291 | [1]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/bf2c1354f55adffadc13b1f1792e205f9dd25f86 292 | [2]: https://community.letsencrypt.org/t/revoking-certain-certificates-on-march-4/114864 293 | [3]: https://letsencrypt.org/docs/rate-limits/ 294 | [4]: https://medium.com/hackernoon/rsa-and-ecdsa-hybrid-nginx-setup-with-letsencrypt-certificates-ee422695d7d3 295 | [5]: https://scotthelme.co.uk/hybrid-rsa-and-ecdsa-certificates-with-nginx/ 296 | [6]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/9195bf02cb200dcec8206b46da971734b1d6669f 297 | [7]: https://letsencrypt.org/docs/certificates-for-localhost/ 298 | [8]: https://pki-tutorial.readthedocs.io/en/latest/cadb.html 299 | [9]: https://support.securly.com/hc/en-us/articles/360008547993-How-to-Install-Securly-s-SSL-Certificate-in-Firefox-on-Windows 300 | [10]: https://gist.github.com/Soarez/9688998 301 | [11]: https://gist.github.com/fntlnz/cf14feb5a46b2eda428e000157447309 302 | [12]: https://github.com/llekn/openssl-ca 303 | [13]: https://jamielinux.com/docs/openssl-certificate-authority/create-the-root-pair.html 304 | [14]: https://github.com/llekn/openssl-ca/blob/master/openssl.cnf 305 | [15]: https://derflounder.wordpress.com/2019/06/06/new-tls-security-requirements-for-ios-13-and-macos-catalina-10-15/ 306 | [16]: https://superuser.com/questions/738612/openssl-ca-keyusage-extension/1248085#1248085 307 | [17]: https://www.feistyduck.com/library/openssl-cookbook/online/ch-openssl.html 308 | [18]: https://nginx.org/en/docs/http/server_names.html 309 | [19]: https://www.globalsign.com/en-ae/blog/google-90-day-certificate-validity-requires-automation 310 | [20]: https://docs.nginx.com/nginx/admin-guide/basic-functionality/runtime-control/#controlling-nginx 311 | [21]: https://nginx.org/en/docs/control.html#reconfiguration 312 | [22]: https://nginx.org/en/docs/control.html#logs 313 | -------------------------------------------------------------------------------- /docs/certbot_authenticators.md: -------------------------------------------------------------------------------- 1 | # Certbot Authenticators 2 | 3 | Certbot allows to use a number of [authenticators to get certificates][1]. By 4 | default, and this will be sufficient for most users, this container uses the 5 | [webroot authenticator][2] which will provision certificates for your domain 6 | names by doing what is called [HTTP-01 validation][3]. Here the ownership of the 7 | domain name is proven by serving a specific content at a given URL, whcih means 8 | that a limitation with this authenticator is that the Let's Encrypt servers 9 | must be able to reach your server on port 80 for it to work. This is the reason 10 | for the ".well-known" location in the 11 | [`redirector.conf`](../src/nginx_conf.d/redirector.conf) file. 12 | 13 | Among the other authenticators available to certbot, the [DNS authenticators][4] 14 | are also available through this container. DNS authenticators allow you to prove 15 | ownership of a domain name by serving a challenge directly through a TXT record 16 | added in your DNS provider. This challenge is called [DNS-01][5] and is a 17 | stronger proof of ownership than using HTTP-01, which is why this method also 18 | allow wildcard certificates (e.g. `*.yourdomain.org`). Using the DNS autheicator 19 | also means that the Let's Encrypt servers only has to perform DNS lookups and do 20 | not need to connect directly to your server. This means that you can get a 21 | "proper" certificate to a service that otherwise is only reachable on your own 22 | LAN. 23 | 24 | 25 | ## Preparing the Container for DNS-01 Challenges 26 | 27 | To use DNS-01 challenges, you will need to create the credentials file for the 28 | chosen authenticator. 29 | 30 | You can find information about how to configure them by following those links 31 | for the supported authenticators: 32 | 33 | - [dns-cloudflare][6] 34 | - [dns-digitalocean][8] 35 | - [dns-dnsimple][9] 36 | - [dns-dnsmadeeasy][10] 37 | - [dns-gehirn][11] 38 | - [dns-google][12] 39 | - [dns-linode][13] 40 | - [dns-luadns][14] 41 | - [dns-nsone][15] 42 | - [dns-ovh][16] 43 | - [dns-rfc2136][17] 44 | - [dns-route53][18] 45 | - [dns-sakuracloud][19] 46 | - [dns-ionos][20] 47 | - [dns-bunny][21] 48 | - [dns-duckdns][22] 49 | - [dns-hetzner][23] 50 | - [dns-infomaniak][24] 51 | - [dns-namecheap][26] 52 | - [dns-godaddy][27] 53 | - [dns-gandi][25] 54 | 55 | You will need to setup the authenticator file at 56 | `$CERTBOT_DNS_CREDENTIALS_DIR/.ini`, where the 57 | `$CERTBOT_DNS_CREDENTIALS_DIR` variable defaults to `/etc/letsencrypt`. 58 | So for e.g. Cloudflare you would need the file `/etc/letsencrypt/cloudflare.ini` 59 | with the following content: 60 | 61 | ```ini 62 | # Cloudflare API token used by Certbot 63 | dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567 64 | ``` 65 | 66 | It is also possible to define unique credentials files by including extra 67 | information in the certificate path, read more about it 68 | [below](#unique-credentials-files). 69 | 70 | 71 | ## Using a DNS-01 Authenticator by Default 72 | 73 | You can use an authenticator solving DNS-01 challenges by default by setting the 74 | `CERTBOT_AUTHENTICATOR` environment variable with the value as the name of the 75 | authenticator you wish to use (e.g. `dns-cloudflare`). 76 | 77 | All the certificates needing renewal or creation will then start using that 78 | authenticator. Make sure, of course, that you've setup the authenticator 79 | correctly, as described above. 80 | 81 | 82 | ## Using a DNS-01 Authenticator for Specific Certificates Only 83 | 84 | You might want to keep using the `webroot` authenticator in most cases, but 85 | need to use a DNS-01 challenge to setup a wildcard certificate for a given 86 | domain. Or you might even have a domain set up on Route53 while your other 87 | domains are on Cloudflare, and you thus are using `dns-cloudflare` as your 88 | default authenticator. 89 | 90 | In such cases, you can specify the authenticator you wish to use in the 91 | certificate path that you are setting up as `ssl_certificate_key` in your 92 | server block of the nginx configuration. In our case, if we want to use 93 | `dns-route53` for a specific certificate, we could be using the following: 94 | 95 | ``` 96 | server { 97 | listen 443 ssl; 98 | server_name yourdomain.org *.yourdomain.org; 99 | ssl_certificate_key /etc/letsencrypt/live/test-name.dns-route53/privkey.pem; 100 | ... 101 | } 102 | ``` 103 | 104 | The script running in the container to renew certificates will automatically 105 | identify that it needs to use the Route53 authenticator here. Of course, you 106 | will need that authenticator to be configured properly in order to be able to 107 | use it. 108 | 109 | This setting is also compatible with the 110 | [multi-certificate setup](./advanced_usage.md#multi-certificate-setup), so an 111 | RSA certificate via Clouflare's authenticator can be specified like this: 112 | 113 | ``` 114 | ssl_certificate_key /etc/letsencrypt/live/test-name.dns-cloudflare.rsa/privkey.pem; 115 | ``` 116 | 117 | ### Unique Credentials Files 118 | An expansion on the unique DNS authenticator feature mentioned above is that 119 | it is possible to add a suffix to it in order to use individual credentials 120 | files for the same DNS provider. In the following example we have two config 121 | files with the following content: 122 | 123 | ``` 124 | server { 125 | listen 443 ssl; 126 | server_name first.org *.first.org; 127 | ssl_certificate_key /etc/letsencrypt/live/c2.dns-cloudflare_1/privkey.pem; 128 | ... 129 | } 130 | ``` 131 | 132 | ``` 133 | server { 134 | listen 443 ssl; 135 | server_name second.org *.second.org; 136 | ssl_certificate_key /etc/letsencrypt/live/c1.dns-cloudflare_2/privkey.pem; 137 | ... 138 | } 139 | ``` 140 | 141 | As you can see they both use the Cloudflare DNS authenticator, but the first 142 | one will use the `/etc/letsencrypt/cloudflare_1.ini` credentials file while the 143 | second one will use `/etc/letsencrypt/cloudflare_2.ini`. This allows us to have 144 | multiple scoped credentials for the DNS, instead of a single one that can 145 | control all domains. 146 | 147 | The limit to this feature is that the suffix may not consist of an `.` or a `-` 148 | since they are used as separators for other things. 149 | 150 | ## Troubleshooting Tips 151 | 152 | DNS propagation is usually quite fast, but depends a lot on caching. This means 153 | that if Let's Encrypt tried to read the challenge recently, it might still hit 154 | a cache returning an older value of the TXT record that was added by certbot. 155 | 156 | If this happens often to you, you can set the `CERTBOT_DNS_PROPAGATION_SECONDS` 157 | environment variable in your docker configuration, to increase the time to wait 158 | for DNS propagation to happen. 159 | 160 | When that environment variable is not set, certbot will use a default value, 161 | which can be found in the documentation of the authenticator of your chosing. 162 | At the time of writing, this default value is of 10 seconds for all of the DNS 163 | authenticators. 164 | 165 | 166 | 167 | 168 | [1]: https://eff-certbot.readthedocs.io/en/stable/using.html#getting-certificates-and-choosing-plugins 169 | [2]: https://eff-certbot.readthedocs.io/en/stable/using.html#webroot 170 | [3]: https://letsencrypt.org/docs/challenge-types/#http-01-challenge 171 | [4]: https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins 172 | [5]: https://letsencrypt.org/docs/challenge-types/#dns-01-challenge 173 | [6]: https://certbot-dns-cloudflare.readthedocs.io/en/stable/#credentials 174 | [8]: https://certbot-dns-digitalocean.readthedocs.io/en/stable/#credentials 175 | [9]: https://certbot-dns-dnsimple.readthedocs.io/en/stable/#credentials 176 | [10]: https://certbot-dns-dnsmadeeasy.readthedocs.io/en/stable/#credentials 177 | [11]: https://certbot-dns-gehirn.readthedocs.io/en/stable/#credentials 178 | [12]: https://certbot-dns-google.readthedocs.io/en/stable/#credentials 179 | [13]: https://certbot-dns-linode.readthedocs.io/en/stable/#credentials 180 | [14]: https://certbot-dns-luadns.readthedocs.io/en/stable/#credentials 181 | [15]: https://certbot-dns-nsone.readthedocs.io/en/stable/#credentials 182 | [16]: https://certbot-dns-ovh.readthedocs.io/en/stable/#credentials 183 | [17]: https://certbot-dns-rfc2136.readthedocs.io/en/stable/#credentials 184 | [18]: https://certbot-dns-route53.readthedocs.io/en/stable/#credentials 185 | [19]: https://certbot-dns-sakuracloud.readthedocs.io/en/stable/#credentials 186 | [20]: https://github.com/helgeerbe/certbot-dns-ionos 187 | [21]: https://github.com/mwt/certbot-dns-bunny 188 | [22]: https://github.com/infinityofspace/certbot_dns_duckdns?tab=readme-ov-file#usage 189 | [23]: https://github.com/ctrlaltcoop/certbot-dns-hetzner?tab=readme-ov-file#credentials 190 | [24]: https://github.com/Infomaniak/certbot-dns-infomaniak 191 | [25]: https://github.com/obynio/certbot-plugin-gandi 192 | [26]: https://github.com/knoxell/certbot-dns-namecheap?tab=readme-ov-file#credentials 193 | [27]: https://github.com/miigotu/certbot-dns-godaddy?tab=readme-ov-file#credentials 194 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 5.6.0 4 | - Added gandi DNS authenticator. 5 | - PR by [@benedicteb][58] 6 | 7 | ### 5.5.0 8 | - Added [namecheap][56] DNS authenticator. 9 | - Added [godaddy][57] DNS authenticator. 10 | 11 | ### 5.4.1 12 | - Added infomaniak DNS authenticator. 13 | - PR by [@bcrickboom][55] 14 | 15 | ### 5.4.0 16 | - Added hetzner DNS authenticator. 17 | - PR by [@protos-gunzinger][54] 18 | 19 | ### 5.3.0 20 | - Added DuckDNS authenticator. 21 | - PR by [@emulatorchen][51] 22 | - Removed deprecated [cloudxns][52] authenticator. 23 | - Changed from [LegacyKeyValueFormat][53] to the recommended format for `ENV` definitions. 24 | 25 | ### 5.2.1 26 | - Update certbot to v2.11.0. 27 | 28 | ### 5.2.0 29 | - Forward the `SIGUSR1` signal to Nginx to tell it to reopen log files. 30 | - PR by [@fredrikekre][50] 31 | 32 | ### 5.1.1 33 | - Update certbot to version 2.10.0 34 | 35 | ### 5.1.0 36 | - Add ability to change validity time of the local CA. 37 | - This variable is not mentioned in the main README since it should not be 38 | used without reading the relevant advanced documentation. 39 | 40 | ### 5.0.1 41 | - Lock certbot version via a `requirements.txt` file. 42 | - Updates to the certbot version will now bump patch version of this repo. 43 | - Added Bunny DNS to available authenticators. 44 | - PR by [@chreniuc][49] 45 | 46 | ### 5.0.0 47 | - We now run `nginx -t` before reloading Nginx. This will hopefully provide better info on 48 | config errors without crashing the container. 49 | - The reason for this being a major version is because technically we alter 50 | core container behavior where previous errors would result in a restart. 51 | - PR by [@stefansundin][48] 52 | 53 | ### 4.3.0 54 | - Parent image is now using Python 3.11 which implements PEP 668, so we have 55 | to allow PIP to "break system packages". 56 | 57 | ### 4.2.1 58 | - Require all certificate files to have a size greater than zero. 59 | 60 | ### 4.2.0 61 | - Add Ionos DNS authenticator plugin 62 | - PR by [@mzbik][47]. 63 | 64 | ### 4.1.0 65 | - Install Bash 5.2.15 from [Debian Bookworm][44]. 66 | - Workaround for [this Bash bug][46] which we also had in the [Alpine image][45]. 67 | - Not using a "backport" repository is not recommended, but right now the only way. 68 | - Added timestamps to the log output we produce. 69 | - This is *technically* a breaking change if someone parses our logs, but I will ignore that. 70 | 71 | ### 4.0.0 72 | - New approach to [implementing IPv6 support][41] for the HTTP-01 challenge. 73 | - Deleted the dedicated server in [`certbot.conf`][43] 74 | - This change *should* be transparent for anyone not having a custom `certbot.conf` file, but is 75 | technically making a breaking change for *someone*, thus a major revision bump. 76 | 77 | ### 3.3.1 78 | - Revert [previous feature][41] after it apparently breaking some setups. 79 | 80 | ### 3.3.0 81 | - Have the server in [`certbot.conf`][42] listen on IPv6 as well. 82 | - PR by [@Meptl][41]. 83 | 84 | ### 3.2.2 85 | - Small syntax fixes recommended by shellcheck. 86 | - PR by [@ericstengard][40]. 87 | 88 | ### 3.2.1 89 | - Small syntax fixes recommended by shellcheck. 90 | - PR by [@ericstengard][39]. 91 | 92 | ### 3.2.0 93 | - Make it possible to override the `CERTBOT_PRODUCTION_URL` and `CERTBOT_STAGING_URL` variables. 94 | - You can now point certbot to whichever ACME server you want. 95 | 96 | ### 3.1.3 97 | - Recover and retry in case of failed `dhparam` creation. 98 | - PR by [@staticfloat][38]. 99 | 100 | ### 3.1.2 101 | - Use latest version of Bash in the Alpine image again. 102 | - The `wait` bug is [fixed][37] since Bash 5.1.10. 103 | 104 | ### 3.1.1 105 | - Small [bugfix][14] for the `dns-route53` authenticator. 106 | - Made so it is only bash that is installed from an older Alpine repository. 107 | - PR by [@dtcooper][36]. 108 | 109 | ### 3.1.0 110 | - Replace `sort -u` with `awk '!a[$0]++'` to keep distinct order of the domain names. 111 | - PR by [@dtcooper][35]. 112 | 113 | ### 3.0.1 114 | - Actually use ECDSA certificates by default. 115 | - Eagerness to deploy latest version this update was forgotten. 116 | 117 | ### 3.0.0 118 | - Add support for DNS-01 challenges. 119 | - Check out the list of all currently [supported authenticators](./certbot_authenticators.md). 120 | - This also means it is now possible to request wildcard certificates! 121 | - PR by [@XaF][32]. 122 | - Make it possible to define which authenticator to use on a certificate basis. 123 | - Like with [ECDSA/RSA](./advanced_usage.md#multi-certificate-setup), you can 124 | [add the authenicator's name](./certbot_authenticators.md#using-a-dns-01-authenticator-for-specific-certificates-only) 125 | in the `cert_name` to override the default. 126 | - PR by [@XaF][32]. 127 | - Make it possible to use same `cert_name` across multiple config files. 128 | - The scripts will remember all domain names associated with the cert name. 129 | - This means you can now use as many config files as you want and have them all point to a single certificate. 130 | - Add [BATS][34]. 131 | - A lot unit tests for the Bash functions we use in the [`util.sh`](../src/scripts/util.sh) file. 132 | - Also add it as a [GitHub action](../.github/workflows/unit_tests.yml). 133 | - A huge thank you to [@XaF][33] for providing the foundation for this. 134 | - Add ability to override found `server_name`. 135 | - By adding a comment on the `server_name` line the script will now use [that instead](./advanced_usage.md#override-server_name). 136 | - This enables you to easily group domains under a common wildcard certificate ([example config](../examples/example_server_overrides.conf)). 137 | - Any server name beginning with '`~`' will be ignored. 138 | - This character means that the server name is a regex, and we cannot use it when requesting certificates. 139 | - Use [ECDSA](./good_to_know.md#ecdsa-and-rsa-certificates) certificates by default. 140 | - You now have to explicitly set `USE_ECDSA=0` to disable this. 141 | - We aren't actually introducing any breaking changes, but such a large change deserves a major release. 142 | - Update documentation. 143 | - Update examples. 144 | 145 | 146 | ### 2.4.1 147 | - Fix missing quotes around variable. 148 | - PR by [@LucianDavies][30]. 149 | - Changed package mirror used by Alpine images. More info in [issue #70][31]. 150 | - Added more documentation. 151 | - Updated the `docker-compose` examples a bit. 152 | 153 | ### 2.4.0 154 | - Create a script that can sign certificates with the help of a 155 | [local certificate authortiy](./advanced_usage#local-ca). 156 | - It is now possible to work completely offline. 157 | - We can now create certificates for `localhost`. 158 | - Restructure and add a lot of documentation. 159 | - `openssl` is now a symlink to `libressl` in the Alpine images. 160 | - This is done to simplify the rest of the scripts since the arguments are 161 | the same. 162 | 163 | ### 2.3.0 164 | - Add support for [ECDSA][27] certificates. 165 | - It is possible to have Nginx serve both ECDSA and RSA certificates at the 166 | same time for the same server. Read more in its 167 | [good to know section](./good_to_know.md#ecdsa-and-rsa-certificates). 168 | - Made so that the the "primary domain"/"cert name" can be 169 | [whatever](./good_to_know.md#how-the-script-add-domain-names-to-certificate-requests) 170 | you want. 171 | - This was actually already possible from [`v0.12`](#012), but it is first 172 | now we allow it. 173 | 174 | ### 2.2.0 175 | - Listen to IPv6 in the [redirector.conf](../src/nginx_conf.d/redirector.conf) 176 | in addition to IPv4. 177 | - PR by [@staticfloat][25]. 178 | - Add `reuseport` in the [redirector.conf](../src/nginx_conf.d/redirector.conf), 179 | which improves latency and parallelization. 180 | - PR by [@staticfloat][26]. 181 | - Add mentions in the changelog to people who have helped with issues. 182 | 183 | ### 2.1.0 184 | - Made the `create_dhparams.sh` script capable of creating missing directories. 185 | - Our small [`/docker-entrypoint.d/40-create-dhparam-folder.sh`][17] script 186 | is therefore no longer necessary. 187 | - Made so that we run `symlink_user_configs` at startup so we do not run into 188 | a [race condition][16] with Nginx. 189 | - Some minor cleanup in the Dockerfiles related to the above changes. 190 | 191 | ### 2.0.1 192 | - There now exist a Dockerfile for building from the Nginx Alpine image as well. 193 | - It is possible to use the Alpine version by appending `-alpine` to any 194 | of the tags from now on. 195 | - There are now so many tags available, see 196 | [dockerhub_tags.md](./dockerhub_tags.md) for the possible combinations. 197 | - NOTE: There exists a bug in Bash 5.1.0, which is described in detail 198 | [here][15]. 199 | - Suggested by [@tudddorrr][24]. 200 | - Small fix to the `create_dhparams.sh` script to handle the use of libressl 201 | in Alpine. 202 | - Added a small sleep in order to mitigate a rare race condition between Nginx 203 | startup and the symlink script. 204 | - Fix an ugly printout in the case when the sleep function exited naturally. 205 | 206 | ### 2.0.0 207 | - Big change on how we recommend users to get their `.conf` files into the 208 | container. 209 | - Created a script that [creates symlinks][10] from `conf.d/` to the files 210 | in `user_conf.d/`. 211 | - Users can now [start the container](../README.md#run-with-docker-run) 212 | without having to build anything. 213 | - Still compatible with [the old way](../README.md#build-it-yourself), but I 214 | still think it's a "major" change. 215 | - Suggested by [@MauriceNino][23]. 216 | - Examples are updated to reflect changes. 217 | - Add more logging. 218 | - Add more `"` around variables for extra safety. 219 | - Big overhaul of how the documentation is structured. 220 | - Even more tags now available on Docker Hub! 221 | - See [dockerhub_tags.md](./dockerhub_tags.md) for the list. 222 | 223 | ### 1.3.0 224 | - Ignore values starting with `data:` and `engine:` when verifying that all 225 | files exists. 226 | - PR by [@bblanchon][1]. 227 | - Add a debug mode which is enabled by setting the environment variable 228 | `DEBUG=1`. 229 | 230 | ### 1.2.0 231 | - Fix dependencies so that it is possible to build in 32-bit ARM architectures. 232 | - Reported by [RtKelleher][11]. 233 | - Added [Dependabot][20] to monitor and update the Dockerfiles. 234 | - PR by [@odin568][19]. 235 | - Added [GitHub Actions/Workflows](../.github/workflows) so that each [tag][2] 236 | now is built for multiple arches ([issue #28][3]). 237 | 238 | ### 1.1.0 239 | - Fix that scripts inside [`/docker-entrypoint.d/`][4] were never run 240 | ([issue #21][5]). 241 | - Found while helping [@isomerpages][21] move from @staticfloats image. 242 | - Fix for issue where the script failed in case the `/etc/letsencrypt/dhparams` 243 | folder was missing. 244 | - Reported by [@pmkyl][6]. 245 | 246 | ### 1.0.0 247 | - Move over to [semantic versioning][7]. 248 | - The version number will now be given like this: `[MAJOR].[MINOR].[PATCH]` 249 | - This is done to signify that I feel like this code is stable, since I have 250 | been running this for quite a while. 251 | - Build from a defined version of Nginx. 252 | - This is done to facilitate a way to lock this container to a more specific 253 | version. 254 | - This also allows us to more often trigger rebuilds of this container on 255 | Docker Hub. 256 | - New tags are available on [Docker Hub][2]. 257 | - There will now be tags on the following form: 258 | - latest 259 | - 1.0.0 260 | - 1.0.0-nginx1.19.7 261 | 262 | ### 0.16 263 | - Container now listens to [`SIGHUP`](./advanced_usage.md#manualforce-renewal) 264 | and will reload all configs if this signal is received. 265 | - More details can be found in the commit message: [bf2c135][13] 266 | - Made Docker image slightly smaller by including `--no-install-recommends`. 267 | - There is now also a [`dev` branch][9]/tag if you are brave and want to run 268 | experimental builds. 269 | - [JonasAlfredsson/docker-nginx-certbot][22] is now its own independent 270 | repository (i.e. no longer just a fork). 271 | 272 | ### 0.15 273 | - It is now possible to 274 | [manually trigger](./advanced_usage.md#manualforce-renewal) a renewal of 275 | certificates. 276 | - It is also possible to include "force" to add `--force-renewal` to the 277 | request. 278 | - The "clean exit" trap now handle that parent container changed to 279 | [`SIGQUIT`][12] as stop signal. 280 | - The "certbot" server block (in Nginx) now prints to stdout by default. 281 | - Massive refactoring of both code and files: 282 | - Our "start **command**" file is now called `start_nginx_certbot.sh` instead 283 | of `entrypoint.sh`. 284 | - Both `create_dhparams.sh` and `run_certbot.sh` can now be run by themselves 285 | inside the container. 286 | - I have added `set -e` in most of the files so the program exit as intended 287 | when unexpected errors occurs. 288 | - Added `{}` and `""` around most of the bash variables. 289 | - Change some log messages and where they appear. 290 | - Our `/scripts/startup/` folder has been removed. 291 | - The parent container will run any `*.sh` file found inside the 292 | [`/docker-entrypoint.d/`][4] folder. 293 | 294 | ### 0.14 295 | - Made so that the container now exits gracefully and reports the correct exit 296 | code. 297 | - More details can be found in the commit message: [43dde6e][8] 298 | - Bash script now correctly monitors **both** the Nginx and the certbot renewal 299 | process PIDs. 300 | - If either one of these processes dies, the container will exit with the same 301 | exit code as that process. 302 | - This will also trigger a graceful exit for the rest of the processes. 303 | - Removed unnecessary and empty `ENTRYPOINT` from Dockerfile. 304 | - A lot of refactoring of the code, cosmetic changes and editing of comments. 305 | 306 | ### 0.13 307 | - Fixed the regex used in all of the `sed` commands. 308 | - Now makes sure that the proper amount of spaces are present in the right 309 | places. 310 | - Now allows comments at the end of the lines in the configs. `# Nice!` 311 | - Made the expression a little bit more readable thanks to the `-r` flag. 312 | - Now made certbot solely responsible for checking if the certificates needs to 313 | be renewed. 314 | - Certbot is actually smart enough to not send any renewal requests if it 315 | doesn't have to. 316 | - The time interval used to trigger the certbot renewal check is now user 317 | configurable. 318 | - The environment variable to use is `RENEWAL_INTERVAL`. 319 | 320 | ### 0.12 321 | - Added `--cert-name` flag to the certbot certificate request command. 322 | - This allows for both adding and subtracting domains to the same certificate 323 | file. 324 | - Makes it possible to have path names that are not domain names (but this 325 | is not allowed yet). 326 | - Made the file parsing functions smarter so they only find unique file paths. 327 | - Cleaned up some log output. 328 | - Updated the `docker-compose` example. 329 | - Fixed some spelling in the documentation. 330 | 331 | ### 0.11 332 | - Python 2 is EOL, so it's time to move over to Python 3. 333 | - From now on [Docker Hub][2] will also automatically build with tags. 334 | - Lock the version by specifying the tag: `jonasal/nginx-certbot:0.11` 335 | 336 | ### 0.10 337 | - Update to new ACME v2 servers. 338 | - PR by [@seaneshbaugh][18]. 339 | 340 | ### 0.9 341 | - I am now confident enough to remove the version suffixes. 342 | - `nginx:mainline` is now using Debian 10 Buster. 343 | - Updated documentation. 344 | 345 | ### 0.9-gamma 346 | - Make both Nginx and the update script child processes of the `entrypoint.sh` 347 | script. 348 | - Container will now die along with Nginx like it should. 349 | - The Diffie-Hellman parameters now have better permissions. 350 | - Container now exist on [Docker Hub][2] under `jonasal/nginx-certbot:latest` 351 | - More documentation. 352 | 353 | ### 0.9-beta 354 | - `@JonasAlfredsson` enters the battle. 355 | - Diffie-Hellman parameters are now automatically generated. 356 | - Nginx now handles everything HTTP related -> certbot set to webroot mode. 357 | - Better checking to see if necessary files exist. 358 | - Will now request a certificate that includes all domain variants listed 359 | on the `server_name` line. 360 | - More extensive documentation. 361 | 362 | ### 0.8 363 | - Ditch cron, it never liked me anyway. Just use `sleep` and a `while` 364 | loop instead. 365 | 366 | ### 0.7 367 | - Complete rewrite, build this image on top of the `nginx` image, and run 368 | `cron`/`certbot` alongside `nginx` so that we can have Nginx configs 369 | dynamically enabled as we get SSL certificates. 370 | 371 | ### 0.6 372 | - Add `nginx_auto_enable.sh` script to `/etc/letsencrypt/` so that users can 373 | bring Nginx up before SSL certs are actually available. 374 | 375 | ### 0.5 376 | - Change the name to `docker-certbot-cron`, update documentation, strip out 377 | even more stuff I don't care about. 378 | 379 | ### 0.4 380 | - Rip out a bunch of stuff because `@staticfloat` is a monster, and likes to 381 | do things his way 382 | 383 | ### 0.3 384 | - Add support for webroot mode. 385 | - Run certbot once with all domains. 386 | 387 | ### 0.2 388 | - Upgraded to use certbot client 389 | - Changed image to use alpine linux 390 | 391 | ### 0.1 392 | - Initial release 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | [1]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/32 401 | [2]: https://hub.docker.com/r/jonasal/nginx-certbot 402 | [3]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/28 403 | [4]: https://github.com/nginxinc/docker-nginx/tree/master/entrypoint 404 | [5]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/21 405 | [6]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/20 406 | [7]: https://semver.org/ 407 | [8]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/43dde6ec24f399fe49729b28ba4892665e3d7078 408 | [9]: https://github.com/JonasAlfredsson/docker-nginx-certbot/tree/dev 409 | [10]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/91f8ecaa613f1e7c0dc4ece38fa8f38a004f61ec 410 | [11]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/24 411 | [12]: https://github.com/nginxinc/docker-nginx/commit/3fb70ddd7094c1fdd50cc83d432643dc10ab6243 412 | [13]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/bf2c1354f55adffadc13b1f1792e205f9dd25f86 413 | [14]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/73f0a43e5aa9075e00e3a4c01bfe8595aae1509f 414 | [15]: https://github.com/JonasAlfredsson/bash_fail-to-wait 415 | [16]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/7c5e2108c89c9da5effda1c499fff6ff84f8b1d3 416 | [17]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/9dfa927cda7244768445067993dc42e23b4e78da 417 | [18]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/11 418 | [19]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/22 419 | [20]: https://dependabot.com/ 420 | [21]: https://github.com/isomerpages/isomer-redirection/pull/143 421 | [22]: https://github.com/JonasAlfredsson/docker-nginx-certbot 422 | [23]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/33 423 | [24]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/35 424 | [25]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/44 425 | [26]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/45 426 | [27]: https://sectigostore.com/blog/ecdsa-vs-rsa-everything-you-need-to-know/ 427 | [28]: https://github.com/JonasAlfredsson/docker-nginx-certbot/blob/master/docs/good_to_know.md#ecdsa-and-rsa-certificates 428 | [29]: https://github.com/JonasAlfredsson/docker-nginx-certbot/blob/master/docs/good_to_know.md#how-the-script-add-domain-names-to-certificate-requests 429 | [30]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/69 430 | [31]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/70 431 | [32]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/ffad612ed5555915c37caa2f4d793c26b3809179 432 | [33]: https://github.com/XaF 433 | [34]: https://github.com/bats-core/bats-core 434 | [35]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/112 435 | [36]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/117 436 | [37]: https://github.com/JonasAlfredsson/bash_fail-to-wait/issues/2 437 | [38]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/127 438 | [39]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/144 439 | [40]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/147 440 | [41]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/159 441 | [42]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/6a6d24b2dfdbae48634f44b9ea0ab776c15053fb 442 | [43]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/a35f3be276e9393217937fc7dc44751751adb5fa#diff-be3f5d8ee45aacc8f6a22dd10332dd9503fe20ea1870a548202bf6e21a8f3815 443 | [44]: https://packages.debian.org/search?keywords=bash 444 | [45]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/3855a173f6ce1bc49318cdc7c3a40e4443e92f3d 445 | [46]: https://github.com/JonasAlfredsson/bash_fail-to-wait 446 | [47]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/168 447 | [48]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/207 448 | [49]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/226 449 | [50]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/d3c20ff199301022ea0dc450bf91a23a51838871 450 | [51]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/281 451 | [52]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/fb06d2761942269d73a0630d4b0312b007027dcc 452 | [53]: https://docs.docker.com/reference/build-checks/legacy-key-value-format/ 453 | [54]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/284 454 | [55]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/307 455 | [56]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/313 456 | [57]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/320 457 | [58]: https://github.com/JonasAlfredsson/docker-nginx-certbot/pull/327 458 | -------------------------------------------------------------------------------- /docs/dockerhub_tags.md: -------------------------------------------------------------------------------- 1 | # Available Image Tags 2 | The `latest` tag will always build the head of the 3 | [master branch][master-branch], so please use a more specific one if you can 4 | since master should not be considered "stable". 5 | 6 | All the tags since `2.0.0` are built for the following architectures: 7 | 8 | - linux/amd64 9 | - linux/386 (:warning: not available for [Alpine][alpine-i386] since Nginx `v1.21.0`) 10 | - linux/arm64 11 | - linux/arm/v7 (:warning: not available for Alpine since [tag `v3.1.2`][alpine-armv7]) 12 | 13 | and it is possible to append `-alpine` to any tag from `2.0.1` to get an Alpine 14 | based image instead. The less specific tags will move as those more specific 15 | are updated. 16 | 17 | 18 | | Major | Minor | Patch | Nginx | 19 | | ----: | ----: | ----: | :----------------- | 20 | | 5 | 5.6 | 5.6.0 | 5.6.0-nginx1.28.0 | 21 | | | | | 5.6.0-nginx1.27.5 | 22 | | | 5.5 | 5.5.0 | 5.5.0-nginx1.27.5 | 23 | | | 5.4 | 5.4.1 | 5.4.1-nginx1.27.5 | 24 | | | | | 5.4.1-nginx1.27.4 | 25 | | | | 5.4.0 | 5.4.0-nginx1.27.4 | 26 | | | | | 5.4.0-nginx1.27.3 | 27 | | | | | 5.4.0-nginx1.27.2 | 28 | | | | | 5.4.0-nginx1.27.0 | 29 | | | 5.3 | 5.3.0 | 5.3.0-nginx1.27.0 | 30 | | | 5.2 | 5.2.1 | 5.2.1-nginx1.27.0 | 31 | | | | 5.2.0 | 5.2.0-nginx1.27.0 | 32 | | | | | 5.2.0-nginx1.26.0 | 33 | | | 5.1 | 5.1.1 | 5.1.1-nginx1.26.0 | 34 | | | | 5.1.0 | 5.1.0-nginx1.26.0 | 35 | | | | | 5.1.0-nginx1.25.5 | 36 | | | | | 5.1.0-nginx1.25.4 | 37 | | | 5.0 | 5.0.1 | 5.0.1-nginx1.25.4 | 38 | | | | | 5.0.1-nginx1.25.3 | 39 | | | | 5.0.0 | 5.0.0-nginx1.25.3 | 40 | | | | | 5.0.0-nginx1.25.2 | 41 | | 4 | 4.3 | 4.3.0 | 4.3.0-nginx1.25.2 | 42 | | | | | 4.3.0-nginx1.25.1 | 43 | | | 4.2 | 4.2.1 | 4.2.1-nginx1.25.0 | 44 | | | | | 4.2.1-nginx1.23.4 | 45 | | | | 4.2.0 | 4.2.0-nginx1.23.4 | 46 | | | | | 4.2.0-nginx1.23.3 | 47 | | | 4.1 | 4.1.0 | 4.1.0-nginx1.23.3 | 48 | | | 4.0 | 4.0.0 | 4.0.0-nginx1.23.3 | 49 | | 3 | 3.3 | 3.3.1 | 3.3.1-nginx1.23.3 | 50 | | | | 3.3.0 | 3.3.0-nginx1.23.3 | 51 | | | 3.2 | 3.2.2 | 3.2.2-nginx1.23.3 | 52 | | | | | 3.2.2-nginx1.23.2 | 53 | | | | 3.2.1 | 3.2.1-nginx1.23.2 | 54 | | | | | 3.2.1-nginx1.23.1 | 55 | | | | 3.2.0 | 3.2.0-nginx1.23.1 | 56 | | | | | 3.2.0-nginx1.23.0 | 57 | | | 3.1 | 3.1.3 | 3.1.3-nginx1.23.0 | 58 | | | | 3.1.2 | 3.1.2-nginx1.23.0 | 59 | | | | | 3.1.2-nginx1.21.6 | 60 | | | | 3.1.1 | 3.1.1-nginx1.21.6 | 61 | | | | 3.1.0 | 3.1.0-nginx1.21.6 | 62 | | | 3.0 | 3.0.1 | 3.0.1-nginx1.21.6 | 63 | | | | | 3.0.1-nginx1.21.5 | 64 | | | | | 3.0.1-nginx1.21.4 | 65 | | | | | 3.0.1-nginx1.21.3 | 66 | | | | 3.0.0 | 3.0.0-nginx1.21.3 | 67 | | 2 | 2.4 | 2.4.1 | 2.4.1-nginx1.21.3 | 68 | | | | | 2.4.1-nginx1.21.1 | 69 | | | | | 2.4.1-nginx1.21.0 | 70 | | | | 2.4.0 | 2.4.0-nginx1.21.0 | 71 | | | 2.3 | 2.3.0 | 2.3.0-nginx1.21.0 | 72 | | | 2.2 | 2.2.0 | 2.2.0-nginx1.21.0 | 73 | | | | | 2.2.0-nginx1.19.10 | 74 | | | 2.1 | 2.1.0 | 2.1.0-nginx1.19.10 | 75 | | | 2.0 | 2.0.1 | 2.0.1-nginx1.19.10 | 76 | | | | 2.0.0 | 2.0.0-nginx1.19.10 | 77 | | | | 1.3.0 | 1.3.0-nginx1.19.10 | 78 | | | | | 1.3.0-nginx1.19.9 | 79 | | | | 1.2.0 | 1.2.0-nginx1.19.9 | 80 | | | | | 1.2.0-nginx1.19.8 | 81 | | | | 1.1.0 | 1.1.0-nginx1.19.8 | 82 | | | | | 1.1.0-nginx1.19.7 | 83 | | | | 1.0.0 | 1.0.0-nginx1.19.7 | 84 | 85 | [master-branch]: https://github.com/JonasAlfredsson/docker-nginx-certbot/tree/master 86 | [alpine-i386]: https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/77 87 | [alpine-armv7]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/3fc2d64d3f20aa2163598e57e59a95a79cde1f37 88 | -------------------------------------------------------------------------------- /docs/good_to_know.md: -------------------------------------------------------------------------------- 1 | # Good to Know 2 | 3 | This document contains information about features and behavior that might be 4 | good to know before you start using this image. Feel free to read it all, but I 5 | recommend the first two sections for everyone. 6 | 7 | ## Initial Testing 8 | In case you are just experimenting with setting this up I suggest you set the 9 | environment variable `STAGING=1`, since this will change the Let's Encrypt 10 | challenge URL to their staging one. This will not give you "*proper*" 11 | certificates, but it has ridiculous high [rate limits][1] compared to the 12 | non-staging [production certificates][2] so you can do more mistakes without 13 | having to worry. You can also add `DEBUG=1` for more verbose logging to better 14 | understand what is going on. 15 | 16 | Include them like this: 17 | ```bash 18 | docker run -it -p 80:80 -p 443:443 \ 19 | --env CERTBOT_EMAIL=your@email.org \ 20 | --env STAGING=1 \ 21 | --env DEBUG=1 \ 22 | jonasal/nginx-certbot:latest 23 | ``` 24 | 25 | Note that when switching to production certificates you either need to remove the 26 | staging certificates or issue a [force renewal](./advanced_usage.md#manualforce-renewal) 27 | since by default certbot will *not* request new certificates if any valid 28 | (staging or production) certificates already exist. 29 | 30 | ## Creating a Server `.conf` File 31 | As an example of a barebone (but functional) SSL server in Nginx you can 32 | look at the file [`example_server.conf`](../examples/example_server.conf) 33 | inside the [`examples/`](../examples) directory. By replacing '`yourdomain.org`' 34 | with your own domain you can actually use this config to quickly test if things 35 | are working properly. When doing this for real you should also change the 36 | certificate paths' 37 | ["test-name"](#how-the-script-add-domain-names-to-certificate-requests) to 38 | something more descriptive. 39 | 40 | Place the modified config inside your [`user_conf.d/`](#the-user_confd-folder) 41 | folder, and then run it as described 42 | [in the main README](../README.md#run-with-docker-run). Let the container do 43 | it's [magic](#diffie-hellman-parameters) for a while, and then try to visit 44 | your domain. You should now be greeted with the string \ 45 | "`Let's Encrypt certificate successfully installed!`". 46 | 47 | The files [already present](../src/nginx_conf.d) inside the container's config 48 | folder are there to handle redirection to HTTPS for all incoming requests that 49 | are not part of the certbot challenge requests, so be careful to not overwrite 50 | these unless you know what you are doing. 51 | 52 | ## The `user_conf.d` Folder 53 | Nginx will, by default, load any file ending with `.conf` from within the 54 | `/etc/nginx/conf.d/` folder. However, this image makes use of one important 55 | [configuration file](../src/nginx_conf.d) which need to be present (unless you 56 | know how to replace it with your own), and host mounting a local folder to 57 | the aforementioned location would shadow this important file. 58 | 59 | To solve this problem I therefore suggest you host mount a local folder to 60 | `/etc/nginx/user_conf.d/` instead, and a part of the management scripts will 61 | [create symlinks][3] from `conf.d/` to the files in `user_conf.d/`. This way 62 | we give users a simple way to just start the container, without having to build 63 | a local image first, while still giving them the opportunity to keep doing it 64 | in the old way like how [`@staticfloat`'s image][5] worked. 65 | 66 | 67 | ## How the Script add Domain Names to Certificate Requests 68 | The included script will go through all configuration files (`*.conf*`) it 69 | finds inside Nginx's `/etc/nginx/conf.d/` folder, and create requests from the 70 | file's content. In every unique file it will find any line that says: 71 | 72 | ``` 73 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 74 | ``` 75 | 76 | and only extract the part which here says "`test-name`". This is the value that 77 | will be provided to the [`--cert-name`][14] argument for certbot, so while you 78 | may set basically any name you want here I suggest you keep it descriptive for 79 | your own sake. 80 | 81 | > ADVANCED: It is possible to add keywords to this certificate name in order 82 | > to use [specific algorithms](./advanced_usage.md#multi-certificate-setup) or 83 | > use other [authenitcators](./certbot_authenticators.md#using-a-dns-01-authenticator-for-specific-certificates-only). 84 | 85 | After this the script will find all the lines that contain `server_name` and 86 | make a list of all the domain names that exist on the same line. So a file 87 | containing something like this: 88 | 89 | ``` 90 | server { 91 | listen 443 ssl; 92 | server_name yourdomain.org www.yourdomain.org; 93 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 94 | ... 95 | } 96 | 97 | server { 98 | listen 443 ssl; 99 | server_name sub.yourdomain.org; 100 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 101 | ... 102 | } 103 | ``` 104 | 105 | will share the same certificate file (i.e. the "test-name" certificate), and 106 | all listed domain variants will be included as valid [alt names][15]. It is 107 | also possible to split these sever blocks into two separate config files, 108 | as the script will keep track of the "test-name" value across the scans and just 109 | add any additional findings to it. So in the end we will get a single request 110 | that looks something like this: 111 | 112 | ``` 113 | certbot --cert-name "test-name" ... -d yourdomain.org -d www.yourdomain.org -d sub.yourdomain.org 114 | ``` 115 | 116 | The scripts are quite powerful when it comes to customizability for defining 117 | what should be included in the request, but this is considered a more advanced 118 | usecase that may be further studied in the 119 | [Override `server_name`](./advanced_usage.md#override-server_name) section of 120 | the Advanced Usage document. 121 | 122 | Furthermore, we support wildcard domain names, but that requires you to use an 123 | authenticator capable of DNS-01 challenges, and more info about that may be 124 | found in the [certbot_authenticators.md](./certbot_authenticators.md) document. 125 | 126 | 127 | ## ECDSA and RSA Certificates 128 | [ECDSA (or ECC)][16] certificates use a newer encryption algorithm than the well 129 | established RSA certificates, and are supposedly more secure while being much 130 | smaller. The downside with these is that they are not supported by all clients 131 | yet, but if you don't expect to serve anything outisde the "Modern" row in 132 | [Mozillas compatibility table][17] you should not hesitate to configure certbot 133 | to request these types of certificates. 134 | 135 | This is achieved by setting the [environment variable](../README.md#optional) 136 | `USE_ECDSA=1` (the default since version 3.0.1), and you can optionally tune 137 | which [curve][18] to use with `ELLIPTIC_CURVE`. If you already have RSA 138 | certificates downloaded you will either have to wait until they expire, or 139 | [force](./advanced_usage.md#manualforce-renewal) a renewal, before this change 140 | takes affect. 141 | 142 | With this option you will create only ECDSA certificates for all of your server 143 | configurations, however, I should mention that there is a way to configure 144 | Nginx to serve both ECDSA and RSA certificates at the same time, but this 145 | is explained further in the 146 | [Advanced Usage](./advanced_usage.md#multi-certificate-setup) document. 147 | 148 | > :warning: If you are using ciphers that depend on RSA they will be 149 | > [silently disabled](https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/247#issuecomment-2128717110) 150 | > if only ECDSA certificates are available. 151 | 152 | 153 | ## Renewal Check Interval 154 | This container will automatically start a certbot certificate renewal check 155 | after the time duration that is defined in the environmental variable 156 | `RENEWAL_INTERVAL` has passed. After certbot has done its stuff, the code will 157 | return and wait the defined time before triggering again. 158 | 159 | This process is very simple, and is just a `while [ true ];` loop with a `sleep` 160 | at the end: 161 | 162 | ```bash 163 | while [ true ]; do 164 | # Run certbot... 165 | sleep "$RENEWAL_INTERVAL" 166 | done 167 | ``` 168 | 169 | So when setting the environmental variable, it is possible to use any string 170 | that is recognized by `sleep`, e.g. `3600` or `60m` or `1h`. Read more about 171 | which values that are allowed in its [manual][4]. 172 | 173 | The default is `8d`, since this allows for multiple retries per month, while 174 | keeping the output in the logs at a very low level. If nothing needs to be 175 | renewed certbot won't do anything, so it should be no problem setting it lower 176 | if you want to. The only thing to think about is to not to make it longer than 177 | one month, because then you would [miss the window][6] where certbot would deem 178 | it necessary to update the certificates. 179 | 180 | ## Diffie-Hellman Parameters 181 | Regarding the Diffie-Hellman parameter it is recommended that you have one for 182 | your server, and in Nginx you define it by including a line that starts with 183 | `ssl_dhparam` in the server block (see 184 | [`example_server.conf`](../examples/example_server.conf)). However, you can 185 | make a config file without it and Nginx will work just fine with ciphers that 186 | don't rely on the Diffie-Hellman key exchange ([more info about ciphers][7]). 187 | 188 | The larger you make these parameters the longer it will take to generate them. 189 | I was unlucky and it took me 65 minutes to generate a 4096 bit parameter on a 190 | really old 3.0GHz CPU. This will vary **greatly** between runs as some 191 | randomness is involved. A 2048 bit parameter, which is still secure today, can 192 | probably be calculated in about 1-3 minutes on a modern CPU (this process will 193 | only have to be done once, since one of these parameters is good for the rest 194 | of your website's lifetime). To modify the size of the parameter you may set the 195 | `DHPARAM_SIZE` environment variable. Default is `2048` if nothing is provided. 196 | 197 | It is also possible to have **all** your server configs point to **the same** 198 | Diffie-Hellman parameter on disk. There is no negative effects in doing this for 199 | home use ([source 1][8] & [source 2][9]). For persistence you should place it 200 | inside the dedicated folder `/etc/letsencrypt/dhparams/`, which is inside the 201 | predefined Docker [volume](../README.md#volumes). There is, however, no 202 | requirement to do so, since a missing parameter will be created where the 203 | config file expects the file to be. But this would mean that the script will 204 | have to re-create these every time you restart the container, which may become 205 | a little bit tedious. 206 | 207 | You can also create this file on a completely different (faster?) computer and 208 | just mount/copy the created file into this container. This is perfectly fine, 209 | since it is nothing "private/personal" about this file. The only thing to 210 | think about in that case would perhaps be to use a folder that is not under 211 | `/etc/letsencrypt/`, since that would otherwise cause a double mount. 212 | 213 | ## Help Migrating from `@staticfloat`'s Image 214 | The two images are not that different when it comes to building/running, since 215 | this repository was originally a fork. So just like in `@staticfloat`'s setup 216 | you need to get your own `*.conf` files into the container's 217 | `/etc/nginx/conf.d/` folder, and then you should be able to start this one 218 | just like you did with his. 219 | 220 | This can either be done by copying your own files into the container at 221 | [build time](../README.md#build-it-yourself), or you can mount a local folder to 222 | [`/etc/nginx/user_conf.d/`](#the-user_confd-folder) and 223 | [run it directly](../README.md#run-with-docker-run). In the former case you need 224 | to make sure you do not accidentally overwrite the two files present in this 225 | repository's [`nginx_conf.d/`](../src/nginx_conf.d) folder, since these are 226 | required in order for certbot to request certificates. 227 | 228 | The only obligatory environment variable for starting this container is the 229 | [`CERTBOT_EMAIL`](../README.md#required) one, just like in `@staticfloat`'s 230 | case, but I have exposed a [couple of more](../README.md#optional) that can be 231 | changed from their defaults if you like. Then there is of course any environment 232 | variables read by the [parent container][11] as well, but those are probably 233 | not as important. 234 | 235 | If you were using [templating][12] before, you should probably look into 236 | ["template" files][13] used by the Nginx parent container, since this is not 237 | something I have personally implemented in mine. 238 | 239 | 240 | 241 | 242 | 243 | [1]: https://letsencrypt.org/docs/staging-environment/ 244 | [2]: https://letsencrypt.org/docs/rate-limits/ 245 | [3]: https://github.com/JonasAlfredsson/docker-nginx-certbot/commit/91f8ecaa613f1e7c0dc4ece38fa8f38a004f61ec 246 | [4]: http://man7.org/linux/man-pages/man1/sleep.1.html 247 | [5]: https://github.com/staticfloat/docker-nginx-certbot 248 | [6]: https://community.letsencrypt.org/t/solved-how-often-to-renew/13678 249 | [7]: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html 250 | [8]: https://security.stackexchange.com/questions/70831/does-dh-parameter-file-need-to-be-unique-per-private-key 251 | [9]: https://security.stackexchange.com/questions/94390/whats-the-purpose-of-dh-parameters 252 | 253 | [11]: https://github.com/nginxinc/docker-nginx 254 | [12]: https://github.com/staticfloat/docker-nginx-certbot#templating 255 | [13]: https://github.com/docker-library/docs/tree/master/nginx#using-environment-variables-in-nginx-configuration-new-in-119 256 | [14]: https://certbot.eff.org/docs/using.html#where-are-my-certificates 257 | [15]: https://www.digicert.com/faq/subject-alternative-name.htm 258 | [16]: https://sectigostore.com/blog/ecdsa-vs-rsa-everything-you-need-to-know/ 259 | [17]: https://wiki.mozilla.org/Security/Server_Side_TLS 260 | [18]: https://security.stackexchange.com/questions/31772/what-elliptic-curves-are-supported-by-browsers/104991#104991 261 | -------------------------------------------------------------------------------- /docs/nginx_tips.md: -------------------------------------------------------------------------------- 1 | # Nginx Tips 2 | 3 | This docuemnt contains some tips on how Nginx can be modified in some different 4 | ways that might be of interest. None of these are required to do, but are more 5 | of nice to know information that I found useful to write down for any potential 6 | future endeavor. 7 | 8 | 9 | ## How Nginx Loads Configs 10 | To understand how Nginx loads any custom configurations we first have to take 11 | a look on the main `nginx.conf` file from the parent image. It has a couple of 12 | standard settings included, but on the last line we can se that it opens the 13 | `/etc/nginx/conf.d/` folder and loads any file that ends with `.conf`. 14 | 15 | ```bash 16 | user nginx; 17 | worker_processes auto; 18 | 19 | error_log /var/log/nginx/error.log notice; 20 | pid /var/run/nginx.pid; 21 | 22 | events { 23 | worker_connections 1024; 24 | } 25 | 26 | http { 27 | include /etc/nginx/mime.types; 28 | default_type application/octet-stream; 29 | 30 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 31 | '$status $body_bytes_sent "$http_referer" ' 32 | '"$http_user_agent" "$http_x_forwarded_for"'; 33 | 34 | access_log /var/log/nginx/access.log main; 35 | 36 | sendfile on; 37 | #tcp_nopush on; 38 | 39 | keepalive_timeout 65; 40 | 41 | #gzip on; 42 | 43 | include /etc/nginx/conf.d/*.conf; # <------------ Extra stuff loaded here 44 | } 45 | ``` 46 | 47 | Files in this folder are being loaded in alphabetical order, so something named 48 | `00-proxy.conf` will be loaded before `10-other.conf`. This i really useful to 49 | know, since it allows you to load common settings used by multiple `server` 50 | blocks that are loaded afterwards. 51 | 52 | However, all of these `.conf` file are loaded within the `http` block in Nginx, 53 | so if you want to change anything outside of this block (e.g. `events`) you 54 | will have to add some sort of [`/docker-entrypoint.d/`][7] script to handle it 55 | before Nginx starts, or you can mount your own custom `nginx.conf` on top of 56 | the default. 57 | 58 | A small disclaimer on the last part is that a host mounted file 59 | (`-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf`) will [not change][8] inside the 60 | container if it is changed on the host. However, if you host mount a directory, 61 | and change any of the files within it, the changes will be visible inside the 62 | container. 63 | 64 | 65 | ## Configuration Inheritance 66 | To keep this explanation simple, yet useful, we begin by stating that the Nginx 67 | configuration is split into four blocks. Variables and settings declared in an 68 | outer block (e.g. the Global block) will be inherited by an inner block (e.g. 69 | the Server block) unless you change it inside this inner block. 70 | 71 | So in the example below I have added comments with the current value of the 72 | [`keepalive_timeout`][9] setting in each block: 73 | 74 | ```bash 75 | # -- Global/main block -- 76 | # keepalive_timeout = 60 (The default value) 77 | http { 78 | # -- HTTP block -- 79 | keepalive_timeout = 30 # The value has now changed to 30 80 | server { 81 | # -- Server block -- 82 | # keepalive_timeout = 30 (value inherited from http block) 83 | location /abc/ { 84 | # -- Location block nbr 1 -- 85 | keepalive_timeout = 50 # The value has now changed to 50 86 | } 87 | location /xyz/ { 88 | # -- Location block nbr 2 -- 89 | # keepalive_timeout = 30 (value inherited from server block) 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | This is pretty straight forward for the settings that are only one value, but 96 | the commonly used [`proxy_set_header`][10] setting can be declared multiple 97 | times in order to add multiple values to it, and [its inheritance][11] works a 98 | bit differently. The following is true of all of the settings that can be 99 | declared multiple times. 100 | 101 | In the example below we want to add two headers to all requests, so we 102 | declare them in the `http` block. This builds a map/dictionary with the 103 | key-value pairs we want, and this will be inherited to all the location blocks. 104 | However, in the first location block we want to **add** another header, but 105 | doing it in this way will instead overwrite the current one with just this new 106 | header. 107 | 108 | ```bash 109 | http { 110 | proxy_set_header key1 value1; 111 | proxy_set_header key2 value2; 112 | server { 113 | # proxy_headers: { 114 | # "key1": "value1" 115 | # "key2": "value2" 116 | # } 117 | location /abc/ { 118 | proxy_set_header key3 value3; 119 | # proxy_headers: { 120 | # "key3": "value3" 121 | # } 122 | } 123 | location /xyz/ { 124 | # proxy_headers: { 125 | # "key1": "value1" 126 | # "key2": "value2" 127 | # } 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | The suggested solution to this problem is to create a separate file with the 134 | "common" headers, and then `include` this file where needed. So in our case we 135 | create the file `/etc/nginx/common_headers` with the following content: 136 | 137 | ``` 138 | proxy_set_header key1 value1; 139 | proxy_set_header key2 value2; 140 | ``` 141 | 142 | and then change the config to the following which would make the special 143 | location block have all the desired headers: 144 | 145 | ```bash 146 | http { 147 | include common_headers; 148 | server { 149 | location /abc/ { 150 | include common_headers; 151 | proxy_set_header key3 value3; 152 | # proxy_headers: { 153 | # "key1": "value1" 154 | # "key2": "value2" 155 | # "key3": "value3" 156 | # } 157 | } 158 | location /xyz/ { 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | 165 | ## Reject Unknown Server Name 166 | When setting up server blocks there exist a setting called `default_server`, 167 | which means that Nginx will use this server block in case it cannot match 168 | the incoming domain name with any of the other `server_name`s in its available 169 | config files. However, a less known fact is that if you do not specify a 170 | `default_server` Nginx will automatically use the [first server block][1] in 171 | its configuration files as the default server. 172 | 173 | This might cause confusion as Nginx could now "accidentally" serve a 174 | completely wrong site without the user knowing it. Luckily HTTPS removes some 175 | of this worry, since the browser will most likely throw an 176 | `SSL_ERROR_BAD_CERT_DOMAIN` if the returned certificate is not valid for the 177 | domain that the browser expected to visit. But if the cert is valid for that 178 | domain as well, then there will be problems. 179 | 180 | If you want to guard yourself against this, and return an error in the case 181 | that the client tries to connect with an unknown server name, you need to 182 | configure a catch-all block that responds in the default case. This is simple 183 | in the non-SSL case, where you can just return `444` which will terminate the 184 | connection immediately. 185 | 186 | ``` 187 | server { 188 | listen 80 default_server; 189 | server_name _; 190 | return 444; 191 | } 192 | ``` 193 | 194 | > NOTE: The [redirector.conf](../src/nginx_conf.d/redirector.conf) should be 195 | the `default_server` for port 80 in this image. 196 | 197 | Unfortunately it is not as simple in the secure HTTPS case, since Nginx would 198 | first need to perform the SSL handshake (which needs a valid certificate) 199 | before it can respond with `444` and drop the connection. To work around this 200 | I found a comment in [this][2] post which mentions that in version `>=1.19.4` 201 | of Nginx you can actually use the [`ssl_reject_handshake`][3] feature to 202 | achieve the same functionality. 203 | 204 | ``` 205 | server { 206 | listen 443 ssl default_server; 207 | ssl_reject_handshake on; 208 | } 209 | ``` 210 | 211 | This will lead to an `SSL_ERROR_UNRECOGNIZED_NAME_ALERT` error in case the 212 | client tries to connect over HTTPS to a server name that is not served by this 213 | instance of Nginx, and the connection will be dropped immediately. 214 | 215 | 216 | ## Add Custom Module 217 | Adding a [custom module][4] to Nginx is not enirely trivial, since most guides 218 | I have found require you to re-complie everything with the desired module 219 | included and thus you cannot make use of the official Docker image to build 220 | upon. However, after some research I found that most of these modules are 221 | possible to compile and load as a [dynamic module][5], which enables us to more 222 | or less just add one file and then change one line in the main `nginx.conf`. 223 | 224 | A complete example of how to do this is available over at 225 | [AxisCommunications/docker-nginx-ldap][6], where a multi-stage Docker build 226 | can be viewed that add the LDAP module to the official Nginx image with 227 | minimal changes to the original. 228 | 229 | 230 | 231 | 232 | 233 | 234 | [1]: https://nginx.org/en/docs/http/request_processing.html 235 | [2]: https://serverfault.com/a/631073 236 | [3]: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_reject_handshake 237 | [4]: https://www.nginx.com/resources/wiki/modules/ 238 | [5]: https://www.nginx.com/blog/compiling-dynamic-modules-nginx-plus/ 239 | [6]: https://github.com/AxisCommunications/docker-nginx-ldap 240 | [7]: https://github.com/nginxinc/docker-nginx/tree/master/entrypoint 241 | [8]: hhttps://medium.com/@jonsbun/why-need-to-be-careful-when-mounting-single-files-into-a-docker-container-4f929340834 242 | [9]: https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive_timeout 243 | [10]: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header 244 | [11]: https://stackoverflow.com/a/32126596 245 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | nginx: 5 | image: jonasal/nginx-certbot:latest 6 | restart: unless-stopped 7 | environment: 8 | - CERTBOT_EMAIL 9 | env_file: 10 | - ./nginx-certbot.env 11 | ports: 12 | - 80:80 13 | - 443:443 14 | volumes: # Storage can be either a 15 | - nginx_secrets:/etc/letsencrypt # Docker managed volume (see list at the bottom) 16 | - ./user_conf.d:/etc/nginx/user_conf.d # or a host mount with a relative or full path. 17 | 18 | volumes: 19 | nginx_secrets: 20 | -------------------------------------------------------------------------------- /examples/example_server.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # Listen to port 443 on both IPv4 and IPv6. 3 | listen 443 ssl default_server reuseport; 4 | listen [::]:443 ssl default_server reuseport; 5 | 6 | # Domain names this server should respond to. 7 | server_name yourdomain.org www.yourdomain.org; 8 | 9 | # Load the certificate files. 10 | ssl_certificate /etc/letsencrypt/live/test-name/fullchain.pem; 11 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 12 | ssl_trusted_certificate /etc/letsencrypt/live/test-name/chain.pem; 13 | 14 | # Load the Diffie-Hellman parameter. 15 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 16 | 17 | return 200 'Let\'s Encrypt certificate successfully installed!'; 18 | add_header Content-Type text/plain; 19 | } 20 | -------------------------------------------------------------------------------- /examples/example_server_multicert.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # Listen to port 443 on both IPv4 and IPv6. 3 | listen 443 ssl default_server reuseport; 4 | listen [::]:443 ssl default_server reuseport; 5 | 6 | # Domain names this server should respond to. 7 | server_name yourdomain.org www.yourdomain.org; 8 | 9 | # Load the ECDSA certificates. 10 | ssl_certificate /etc/letsencrypt/live/test-ecc/fullchain.pem; 11 | ssl_certificate_key /etc/letsencrypt/live/test-ecc/privkey.pem; 12 | 13 | # Load the RSA certificates. 14 | ssl_certificate /etc/letsencrypt/live/test-rsa/fullchain.pem; 15 | ssl_certificate_key /etc/letsencrypt/live/test-rsa/privkey.pem; 16 | 17 | # Load the Diffie-Hellman parameter. 18 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 19 | 20 | # Define the ciphers to use in the preferred order. 21 | ssl_protocols TLSv1.2 TLSv1.3; 22 | ssl_prefer_server_ciphers on; 23 | ssl_ciphers "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:DHE+AESGCM:DHE:!RSA!aNULL:!eNULL:!LOW:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!CAMELLIA:!SEED"; 24 | 25 | return 200 'Let\'s Encrypt certificate successfully installed!'; 26 | add_header Content-Type text/plain; 27 | } 28 | -------------------------------------------------------------------------------- /examples/example_server_overrides.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # Listen to port 443 on both IPv4 and IPv6. 3 | listen 443 ssl; 4 | listen [::]:443 ssl; 5 | 6 | # Domain names this server should respond to. 7 | server_name yourdomain.org; 8 | server_name www.yourdomain.org; # certbot_domain:*.yourdomain.org 9 | 10 | # Load the certificate files. 11 | ssl_certificate /etc/letsencrypt/live/test-name/fullchain.pem; 12 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 13 | ssl_trusted_certificate /etc/letsencrypt/live/test-name/chain.pem; 14 | 15 | # Load the Diffie-Hellman parameter. 16 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 17 | 18 | return 200 'You have reached either yourdomain.org or www.yourdomain.org'; 19 | add_header Content-Type text/plain; 20 | } 21 | 22 | server { 23 | listen 443 ssl; 24 | listen [::]:443 ssl; 25 | 26 | server_name sub1.yourdomain.org sub2.yourdomain.org; # certbot_domain:*.yourdomain.org 27 | 28 | ssl_certificate /etc/letsencrypt/live/test-name/fullchain.pem; 29 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 30 | ssl_trusted_certificate /etc/letsencrypt/live/test-name/chain.pem; 31 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 32 | 33 | return 200 'You have reached either sub1.yourdomain.org or sub2.yourdomain.org'; 34 | add_header Content-Type text/plain; 35 | } 36 | 37 | server { 38 | listen 443 ssl; 39 | listen [::]:443 ssl; 40 | 41 | # Server names that start with ~ will be ignored, and in this example the 42 | # "test-name" certificate will already include *.yourdomain.org which will 43 | # cover all the cases here. 44 | server_name ~^(?.+)\.yourdomain\.org$; 45 | 46 | ssl_certificate /etc/letsencrypt/live/test-name/fullchain.pem; 47 | ssl_certificate_key /etc/letsencrypt/live/test-name/privkey.pem; 48 | ssl_trusted_certificate /etc/letsencrypt/live/test-name/chain.pem; 49 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 50 | 51 | location / { 52 | # Return content based on the capture group in the server name. 53 | # Note: "sub1" and "sub2" will be caught by the sever above. 54 | root /content/$user; 55 | } 56 | } 57 | 58 | server { 59 | # Drop any request that does not match any of the other server names. 60 | listen 443 ssl default_server; 61 | ssl_reject_handshake on; 62 | } 63 | -------------------------------------------------------------------------------- /examples/nginx-certbot.env: -------------------------------------------------------------------------------- 1 | # Required 2 | CERTBOT_EMAIL=your@email.org 3 | 4 | # Optional (Defaults) 5 | DHPARAM_SIZE=2048 6 | ELLIPTIC_CURVE=secp256r1 7 | RENEWAL_INTERVAL=8d 8 | RSA_KEY_SIZE=2048 9 | STAGING=0 10 | USE_ECDSA=1 11 | 12 | # Advanced (Defaults) 13 | CERTBOT_AUTHENTICATOR=webroot 14 | CERTBOT_DNS_PROPAGATION_SECONDS="" 15 | DEBUG=0 16 | USE_LOCAL_CA=0 17 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.28.0 2 | LABEL maintainer="Jonas Alfredsson " 3 | 4 | ENV CERTBOT_DNS_AUTHENTICATORS="\ 5 | cloudflare \ 6 | digitalocean \ 7 | dnsimple \ 8 | dnsmadeeasy \ 9 | gehirn \ 10 | google \ 11 | linode \ 12 | luadns \ 13 | nsone \ 14 | ovh \ 15 | rfc2136 \ 16 | route53 \ 17 | sakuracloud \ 18 | ionos \ 19 | bunny \ 20 | duckdns \ 21 | hetzner \ 22 | infomaniak \ 23 | namecheap \ 24 | godaddy \ 25 | gandi \ 26 | " 27 | 28 | # Needed in order to install Python packages via PIP after PEP 668 was 29 | # introduced, but I believe this is safe since we are in a container without 30 | # any real need to cater to other programs/environments. 31 | ARG PIP_BREAK_SYSTEM_PACKAGES=1 32 | 33 | # We need to do some platfrom specific workarounds in the build script, so bring 34 | # this information in to the build environment. 35 | ARG TARGETPLATFORM 36 | 37 | # Through this we gain the ability to handle certbot upgrades through 38 | # dependabot pull requests. 39 | COPY requirements.txt /requirements.txt 40 | 41 | # Do a single run command to make the intermediary containers smaller. 42 | RUN set -ex && \ 43 | # Install packages necessary during the build phase (for all architectures). 44 | apt-get update && \ 45 | apt-get install -y --no-install-recommends \ 46 | build-essential \ 47 | curl \ 48 | libffi8 \ 49 | libffi-dev \ 50 | libssl-dev \ 51 | openssl \ 52 | pkg-config \ 53 | procps \ 54 | python3 \ 55 | python3-dev \ 56 | && \ 57 | # Install the latest version of rustc/cargo if we are in an architecture that 58 | # needs to build the cryptography Python package. 59 | if echo "$TARGETPLATFORM" | grep -E -q '^(linux/386|linux/arm64|linux/arm/v7)'; then \ 60 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | /bin/sh -s -- -y \ 61 | # For some reason the rustup script is unable to correctly identify the 62 | # environment if we are building an i386 image on an x86_64 system, so we need 63 | # to provide this information manually. 64 | $(if [ "$TARGETPLATFORM" = "linux/386" ]; then \ 65 | echo "--default-host i686-unknown-linux-gnu"; \ 66 | fi) && \ 67 | . "$HOME/.cargo/env"; \ 68 | fi && \ 69 | # Install the latest version of PIP, Setuptools and Wheel. 70 | curl -L 'https://bootstrap.pypa.io/get-pip.py' | python3 && \ 71 | # Install certbot. 72 | pip3 install -r /requirements.txt && \ 73 | # And the supported extra authenticators. 74 | pip3 install $(echo $CERTBOT_DNS_AUTHENTICATORS | sed 's/\(^\| \)/\1certbot-dns-/g') && \ 75 | # Remove everything that is no longer necessary. 76 | apt-get remove --purge -y \ 77 | build-essential \ 78 | curl \ 79 | libffi-dev \ 80 | libssl-dev \ 81 | pkg-config \ 82 | python3-dev \ 83 | && \ 84 | if echo "$TARGETPLATFORM" | grep -E -q '^(linux/386|linux/arm64|linux/arm/v7)'; then \ 85 | rustup self uninstall -y; \ 86 | fi && \ 87 | apt-get autoremove -y && \ 88 | apt-get clean && \ 89 | rm -rf /var/lib/apt/lists/* && \ 90 | rm -rf /root/.cache && \ 91 | # Create new directories and set correct permissions. 92 | mkdir -p /var/www/letsencrypt && \ 93 | mkdir -p /etc/nginx/user_conf.d && \ 94 | chown www-data:www-data -R /var/www \ 95 | && \ 96 | # Make sure there are no surprise config files inside the config folder. 97 | rm -f /etc/nginx/conf.d/* 98 | 99 | # Copy in our "default" Nginx server configurations, which make sure that the 100 | # ACME challenge requests are correctly forwarded to certbot and then redirects 101 | # everything else to HTTPS. 102 | COPY nginx_conf.d/ /etc/nginx/conf.d/ 103 | 104 | # Copy in all our scripts and make them executable. 105 | COPY scripts/ /scripts 106 | RUN chmod +x -R /scripts && \ 107 | # Make so that the parent's entrypoint script is properly triggered (issue #21). 108 | sed -ri '/^if \[ "\$1" = "nginx" \] \|\| \[ "\$1" = "nginx-debug" \]; then$/,${s//if echo "$1" | grep -q "nginx"; then/;b};$q1' /docker-entrypoint.sh 109 | 110 | # Create a volume to have persistent storage for the obtained certificates. 111 | VOLUME /etc/letsencrypt 112 | 113 | # The Nginx parent Docker image already expose port 80, so we only need to add 114 | # port 443 here. 115 | EXPOSE 443 116 | 117 | # Change the container's start command to launch our Nginx and certbot 118 | # management script. 119 | CMD [ "/scripts/start_nginx_certbot.sh" ] 120 | -------------------------------------------------------------------------------- /src/Dockerfile-alpine: -------------------------------------------------------------------------------- 1 | FROM nginx:1.28.0-alpine 2 | LABEL maintainer="Jonas Alfredsson " 3 | 4 | ENV CERTBOT_DNS_AUTHENTICATORS="\ 5 | cloudflare \ 6 | digitalocean \ 7 | dnsimple \ 8 | dnsmadeeasy \ 9 | gehirn \ 10 | google \ 11 | linode \ 12 | luadns \ 13 | nsone \ 14 | ovh \ 15 | rfc2136 \ 16 | route53 \ 17 | sakuracloud \ 18 | ionos \ 19 | bunny \ 20 | duckdns \ 21 | hetzner \ 22 | infomaniak \ 23 | namecheap \ 24 | godaddy \ 25 | gandi \ 26 | " 27 | 28 | # Needed in order to install Python packages via PIP after PEP 668 was 29 | # introduced, but I believe this is safe since we are in a container without 30 | # any real need to cater to other programs/environments. 31 | ARG PIP_BREAK_SYSTEM_PACKAGES=1 32 | 33 | # Through this we gain the ability to handle certbot upgrades through 34 | # dependabot pull requests. 35 | COPY requirements.txt /requirements.txt 36 | 37 | # Do a single run command to make the intermediary containers smaller. 38 | RUN set -ex && \ 39 | # Install packages necessary during the build phase (for all architectures). 40 | apk add --no-cache \ 41 | bash \ 42 | curl \ 43 | findutils \ 44 | libffi \ 45 | libffi-dev \ 46 | libressl \ 47 | libressl-dev \ 48 | ncurses \ 49 | procps \ 50 | python3 \ 51 | python3-dev \ 52 | sed \ 53 | && \ 54 | # Install the latest version of PIP, Setuptools and Wheel. 55 | curl -L 'https://bootstrap.pypa.io/get-pip.py' | python3 && \ 56 | # Install certbot. 57 | pip3 install -r /requirements.txt && \ 58 | # And the supported extra authenticators. 59 | pip3 install $(echo $CERTBOT_DNS_AUTHENTICATORS | sed 's/\(^\| \)/\1certbot-dns-/g') && \ 60 | # Remove everything that is no longer necessary. 61 | apk del \ 62 | curl \ 63 | libffi-dev \ 64 | libressl-dev \ 65 | python3-dev \ 66 | && \ 67 | rm -rf /root/.cache && \ 68 | # Create new directories and set correct permissions. 69 | mkdir -p /var/www/letsencrypt && \ 70 | mkdir -p /etc/nginx/user_conf.d && \ 71 | chown 82:82 -R /var/www \ 72 | && \ 73 | # Symlink libressl so it is invoked when "openssl" is called by the scripts. 74 | ln -s /usr/bin/libressl /usr/bin/openssl \ 75 | && \ 76 | # Make sure there are no surprise config files inside the config folder. 77 | rm -f /etc/nginx/conf.d/* 78 | 79 | # Copy in our "default" Nginx server configurations, which make sure that the 80 | # ACME challenge requests are correctly forwarded to certbot and then redirects 81 | # everything else to HTTPS. 82 | COPY nginx_conf.d/ /etc/nginx/conf.d/ 83 | 84 | # Copy in all our scripts and make them executable. 85 | COPY scripts/ /scripts 86 | RUN chmod +x -R /scripts && \ 87 | # Make so that the parent's entrypoint script is properly triggered (issue #21). 88 | sed -ri '/^if \[ "\$1" = "nginx" \] \|\| \[ "\$1" = "nginx-debug" \]; then$/,${s//if echo "$1" | grep -q "nginx"; then/;b};$q1' /docker-entrypoint.sh 89 | 90 | # Create a volume to have persistent storage for the obtained certificates. 91 | VOLUME /etc/letsencrypt 92 | 93 | # The Nginx parent Docker image already expose port 80, so we only need to add 94 | # port 443 here. 95 | EXPOSE 443 96 | 97 | # Change the container's start command to launch our Nginx and certbot 98 | # management script. 99 | CMD [ "/scripts/start_nginx_certbot.sh" ] 100 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | build: Makefile Dockerfile 4 | docker build --progress=plain -t jonasal/nginx-certbot:local . 5 | 6 | run: 7 | docker run -it --rm \ 8 | -e CERTBOT_EMAIL=user@example.com \ 9 | -e DEBUG=1 \ 10 | jonasal/nginx-certbot:local 11 | 12 | build-alpine: Makefile Dockerfile 13 | docker build --progress=plain -t jonasal/nginx-certbot:local-alpine -f ./Dockerfile-alpine . 14 | 15 | run-alpine: 16 | docker run -it --rm \ 17 | -e CERTBOT_EMAIL=user@example.com \ 18 | -e DEBUG=1 \ 19 | jonasal/nginx-certbot:local-alpine 20 | 21 | # These commands are primarily used for development, see link for more info: 22 | # https://github.com/JonasAlfredsson/docker-nginx-certbot/issues/28 23 | dev: 24 | docker buildx build --platform linux/amd64,linux/386,linux/arm64,linux/arm/v7 --tag jonasal/nginx-certbot:dev -f ./Dockerfile ./ 25 | 26 | dev-alpine: 27 | docker buildx build --platform linux/amd64,linux/arm64 --tag jonasal/nginx-certbot:dev-alpine -f ./Dockerfile-alpine ./ 28 | 29 | push: 30 | docker buildx build --platform linux/amd64,linux/arm64 --tag jonasal/nginx-certbot:dev --pull --no-cache --push . 31 | -------------------------------------------------------------------------------- /src/nginx_conf.d/redirector.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # Listen on plain old HTTP and catch all requests so they can be redirected 3 | # to HTTPS instead. 4 | listen 80 default_server reuseport; 5 | listen [::]:80 default_server reuseport; 6 | 7 | # Anything requesting this particular URL should be served content from 8 | # Certbot's folder so the HTTP-01 ACME challenges can be completed for the 9 | # HTTPS certificates. 10 | location '/.well-known/acme-challenge' { 11 | default_type "text/plain"; 12 | root /var/www/letsencrypt; 13 | } 14 | 15 | # Everything else gets shunted over to HTTPS for each user defined 16 | # server to handle. 17 | location / { 18 | return 301 https://$http_host$request_uri; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | certbot==4.1.0 2 | -------------------------------------------------------------------------------- /src/scripts/create_dhparams.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Source in util.sh so we can have our nice tools. 5 | . "$(cd "$(dirname "$0")"; pwd)/util.sh" 6 | 7 | # This method may take an extremely long time to complete, be patient. 8 | # It should be possible to use the same dhparam file for all sites, just 9 | # specify the same file path under the "ssl_dhparam" parameter in the Nginx 10 | # server config. 11 | # The created file should be stored somewhere under /etc/letsencrypt/dhparams/ 12 | # to ensure persistence between restarts. 13 | create_dhparam() { 14 | if [ -z "${DHPARAM_SIZE}" ]; then 15 | debug "DHPARAM_SIZE unset, using default of 2048 bits" 16 | DHPARAM_SIZE=2048 17 | fi 18 | 19 | info " 20 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 21 | % ATTENTION! % 22 | % % 23 | % This script will now create a ${DHPARAM_SIZE} bit Diffie-Hellman % 24 | % parameter to use during the SSL handshake. % 25 | % % 26 | % >>>>> This MIGHT take a VERY long time! <<<<< % 27 | % (Took 65 minutes for 4096 bit on an old 3GHz CPU) % 28 | % % 29 | % However, there is some randomness involved so it might % 30 | % be both faster or slower for you. 2048 is secure enough % 31 | % for today and quite fast to generate. These files will % 32 | % only have to be created once so please be patient. % 33 | % A message will be displayed when this process finishes. % 34 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 35 | " 36 | info "Will now output to the following file: '${1}'" 37 | openssl dhparam -out "${1}" "${DHPARAM_SIZE}" 38 | info " 39 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 40 | % >>>>> Diffie-Hellman parameter creation done! <<<<< % 41 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 42 | " 43 | } 44 | 45 | # Find any mentions of Diffie-Hellman parameters and create them if missing. 46 | for conf_file in /etc/nginx/conf.d/*.conf*; do 47 | for dh_file in $(parse_dhparams "${conf_file}"); do 48 | if [ ! -f "${dh_file}" ] || ! grep -q "END DH PARAMETERS" "${dh_file}"; then 49 | warning "Couldn't find the dhparam file '${dh_file}'; creating it..." 50 | mkdir -vp "$(dirname "${dh_file}")" 51 | rm -f "${dh_file}" 52 | create_dhparam "${dh_file}" 53 | chmod 600 "${dh_file}" 54 | fi 55 | done 56 | done 57 | -------------------------------------------------------------------------------- /src/scripts/run_certbot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # URLs used when requesting certificates. 5 | # These are picked up from the environment if they are set, which enables 6 | # advanced usage of custom ACME servers, else it will use the default Let's 7 | # Encrypt servers defined here. 8 | : "${CERTBOT_PRODUCTION_URL=https://acme-v02.api.letsencrypt.org/directory}" 9 | : "${CERTBOT_STAGING_URL=https://acme-staging-v02.api.letsencrypt.org/directory}" 10 | 11 | # Source in util.sh so we can have our nice tools. 12 | . "$(cd "$(dirname "$0")"; pwd)/util.sh" 13 | 14 | info "Starting certificate renewal process" 15 | 16 | # We require an email to be able to request a certificate. 17 | if [ -z "${CERTBOT_EMAIL}" ]; then 18 | error "CERTBOT_EMAIL environment variable undefined; certbot will do nothing!" 19 | exit 1 20 | fi 21 | 22 | # Use the correct challenge URL depending on if we want staging or not. 23 | if [ "${STAGING}" = "1" ]; then 24 | debug "Using staging environment" 25 | letsencrypt_url="${CERTBOT_STAGING_URL}" 26 | else 27 | debug "Using production environment" 28 | letsencrypt_url="${CERTBOT_PRODUCTION_URL}" 29 | fi 30 | 31 | # Ensure that an RSA key size is set. 32 | if [ -z "${RSA_KEY_SIZE}" ]; then 33 | debug "RSA_KEY_SIZE unset, defaulting to 2048" 34 | RSA_KEY_SIZE=2048 35 | fi 36 | 37 | # Ensure that an elliptic curve is set. 38 | if [ -z "${ELLIPTIC_CURVE}" ]; then 39 | debug "ELLIPTIC_CURVE unset, defaulting to 'secp256r1'" 40 | ELLIPTIC_CURVE="secp256r1" 41 | fi 42 | 43 | # Ensure that we have a directory where DNS credentials may be placed. 44 | : "${CERTBOT_DNS_CREDENTIALS_DIR=/etc/letsencrypt}" 45 | if [ ! -d "${CERTBOT_DNS_CREDENTIALS_DIR}" ]; then 46 | error "DNS credentials directory '${CERTBOT_DNS_CREDENTIALS_DIR}' does not exist" 47 | exit 1 48 | fi 49 | 50 | if [ "${1}" = "force" ]; then 51 | info "Forcing renewal of certificates" 52 | force_renew="--force-renewal" 53 | fi 54 | 55 | # Helper function to ask certbot to request a certificate for the given cert 56 | # name. The CERTBOT_EMAIL environment variable must be defined, so that 57 | # Let's Encrypt may contact you in case of security issues. 58 | # 59 | # $1: The name of the certificate (e.g. domain.rsa.dns-rfc2136) 60 | # $2: String with all requested domains (e.g. -d domain.org -d www.domain.org) 61 | # $3: Type of key algorithm to use (rsa or ecdsa) 62 | get_certificate() { 63 | local cert_name="${1}" 64 | local authenticator="" 65 | local authenticator_params="" 66 | local challenge_type="" 67 | local dns_config_file="" 68 | 69 | # Determine the authenticator to use to solve the authentication challenge. 70 | # Having the authenticator specified in the certificate name will take 71 | # precedence over the environmental variable. 72 | if [[ "${cert_name,,}" =~ (^|[-.])webroot([-.]|$) ]]; then 73 | debug "Found mention of 'webroot' in name '${cert_name}" 74 | authenticator="webroot" 75 | elif [[ "${cert_name,,}" =~ (^|[-.])dns-([^-.]*)([-.]|$) ]]; then 76 | # Looks like there is some kind of DNS authenticator in the name, save the full name as the 77 | # config file. This allows something like "name.dns-rfc2136_conf1.ecc" to then use the 78 | # config file name "rfc2136_conf1.ini" instead of just "rfc2136.ini" further down. 79 | dns_config_file="${CERTBOT_DNS_CREDENTIALS_DIR}/${BASH_REMATCH[2]}.ini" 80 | if [[ "${BASH_REMATCH[2]}" =~ ($(echo ${CERTBOT_DNS_AUTHENTICATORS} | sed 's/ /|/g')) ]]; then 81 | authenticator="dns-${BASH_REMATCH[1]}" 82 | debug "Found mention of authenticator '${authenticator}' in name '${cert_name}'" 83 | else 84 | error "The DNS authenticator found in '${cert_name}' does not appear to be supported" 85 | return 1 86 | fi 87 | elif [ -n "${CERTBOT_AUTHENTICATOR}" ]; then 88 | authenticator="${CERTBOT_AUTHENTICATOR}" 89 | else 90 | authenticator="webroot" 91 | fi 92 | 93 | # Add correct parameters for the different authenticator types. 94 | if [ "${authenticator}" == "webroot" ]; then 95 | challenge_type="http-01" 96 | authenticator_params="--webroot-path=/var/www/letsencrypt" 97 | elif [[ "${authenticator}" =~ ^dns-($(echo ${CERTBOT_DNS_AUTHENTICATORS} | sed 's/ /|/g'))$ ]]; then 98 | challenge_type="dns-01" 99 | 100 | if [ "${authenticator#dns-}" == "route53" ]; then 101 | # This one is special and makes use of a different configuration. 102 | if [[ ( -z "${AWS_ACCESS_KEY_ID}" || -z "${AWS_SECRET_ACCESS_KEY}" ) && ! -f "${HOME}/.aws/config" ]]; then 103 | error "Authenticator is '${authenticator}' but neither '${HOME}/.aws/config' or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY are found" 104 | return 1 105 | fi 106 | else 107 | if [ -z "${dns_config_file}" ]; then 108 | # If we don't already have a config file set for this authenticator we assemble 109 | # the default path. 110 | dns_config_file="${CERTBOT_DNS_CREDENTIALS_DIR}/${authenticator#dns-}.ini" 111 | fi 112 | if [ ! -f "${dns_config_file}" ]; then 113 | error "Authenticator is '${authenticator}' but '${dns_config_file}' is missing" 114 | return 1 115 | fi 116 | debug "Using DNS credentials file at '${dns_config_file}'" 117 | authenticator_params="--${authenticator}-credentials=${dns_config_file}" 118 | fi 119 | 120 | if [ -n "${CERTBOT_DNS_PROPAGATION_SECONDS}" ]; then 121 | authenticator_params="${authenticator_params} --${authenticator}-propagation-seconds=${CERTBOT_DNS_PROPAGATION_SECONDS}" 122 | fi 123 | else 124 | error "Unknown authenticator '${authenticator}' for '${cert_name}'" 125 | return 1 126 | fi 127 | 128 | info "Requesting an ${3^^} certificate for '${cert_name}' (${challenge_type} through ${authenticator})" 129 | certbot certonly \ 130 | --agree-tos --keep -n --text \ 131 | --preferred-challenges ${challenge_type} \ 132 | --authenticator ${authenticator} \ 133 | ${authenticator_params} \ 134 | --email "${CERTBOT_EMAIL}" \ 135 | --server "${letsencrypt_url}" \ 136 | --rsa-key-size "${RSA_KEY_SIZE}" \ 137 | --elliptic-curve "${ELLIPTIC_CURVE}" \ 138 | --key-type "${3}" \ 139 | --cert-name "${cert_name}" \ 140 | ${2} \ 141 | --debug ${force_renew} 142 | } 143 | 144 | # Get all the cert names for which we should create certificate requests and 145 | # have them signed, along with the corresponding server names. 146 | # 147 | # This will return an associative array that looks something like this: 148 | # "cert_name" => "server_name1 server_name2" 149 | declare -A certificates 150 | while IFS= read -r -d $'\0' conf_file; do 151 | parse_config_file "${conf_file}" certificates 152 | done < <(find -L /etc/nginx/conf.d/ -name "*.conf*" -type f -print0) 153 | 154 | # Iterate over each key and make a certificate request for them. 155 | for cert_name in "${!certificates[@]}"; do 156 | server_names=(${certificates["$cert_name"]}) 157 | 158 | # Determine which type of key algorithm to use for this certificate 159 | # request. Having the algorithm specified in the certificate name will 160 | # take precedence over the environmental variable. 161 | if [[ "${cert_name,,}" =~ (^|[-.])ecdsa([-.]|$) ]]; then 162 | debug "Found variant of 'ECDSA' in name '${cert_name}" 163 | key_type="ecdsa" 164 | elif [[ "${cert_name,,}" =~ (^|[-.])ecc([-.]|$) ]]; then 165 | debug "Found variant of 'ECC' in name '${cert_name}" 166 | key_type="ecdsa" 167 | elif [[ "${cert_name,,}" =~ (^|[-.])rsa([-.]|$) ]]; then 168 | debug "Found variant of 'RSA' in name '${cert_name}" 169 | key_type="rsa" 170 | elif [ "${USE_ECDSA}" == "0" ]; then 171 | key_type="rsa" 172 | else 173 | key_type="ecdsa" 174 | fi 175 | 176 | # Assemble the list of domains to be included in the request from 177 | # the parsed 'server_names' 178 | domain_request="" 179 | for server_name in "${server_names[@]}"; do 180 | domain_request="${domain_request} -d ${server_name}" 181 | done 182 | 183 | # Hand over all the info required for the certificate request, and 184 | # let certbot decide if it is necessary to update the certificate. 185 | if ! get_certificate "${cert_name}" "${domain_request}" "${key_type}"; then 186 | error "Certbot failed for '${cert_name}'. Check the logs for details." 187 | fi 188 | done 189 | 190 | # After trying to get all our certificates, auto enable any configs that we 191 | # did indeed get certificates for. 192 | auto_enable_configs 193 | 194 | # Make sure the Nginx configs are valid. 195 | if ! nginx -t; then 196 | error "Nginx configuration is invalid, skipped reloading. Check the logs for details." 197 | exit 0 198 | fi 199 | 200 | # Finally, tell Nginx to reload the configs. 201 | nginx -s reload 202 | -------------------------------------------------------------------------------- /src/scripts/run_local_ca.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Important files necessary for this script to work. The LOCAL_CA_DIR variable 5 | # is read from the environment if it is set, else it will use the default 6 | # provided here. 7 | : ${LOCAL_CA_DIR:="/etc/local_ca"} 8 | LOCAL_CA_KEY="${LOCAL_CA_DIR}/caPrivkey.pem" 9 | LOCAL_CA_CRT="${LOCAL_CA_DIR}/caCert.pem" 10 | LOCAL_CA_DB="${LOCAL_CA_DIR}/index.txt" 11 | LOCAL_CA_SRL="${LOCAL_CA_DIR}/serial.txt" 12 | LOCAL_CA_CRT_DIR="${LOCAL_CA_DIR}/new_certs" 13 | 14 | # Source in util.sh so we can have our nice tools. 15 | . "$(cd "$(dirname "$0")"; pwd)/util.sh" 16 | 17 | info "Starting certificate renewal process with local CA" 18 | 19 | # We require an email to be set here as well, in order to simulate how it would 20 | # be in the real certbot case. 21 | if [ -z "${CERTBOT_EMAIL}" ]; then 22 | error "CERTBOT_EMAIL environment variable undefined; local CA will do nothing!" 23 | exit 1 24 | fi 25 | 26 | # Ensure that an RSA key size is set. 27 | if [ -z "${RSA_KEY_SIZE}" ]; then 28 | debug "RSA_KEY_SIZE unset, defaulting to 2048" 29 | RSA_KEY_SIZE=2048 30 | fi 31 | 32 | # This is an OpenSSL configuration file that has settings for creating a well 33 | # configured CA, as well as server certificates that adhere to the strict 34 | # standards of web browsers. This is not complete, but will have the missing 35 | # sections dynamically assembled by the functions that need them at runtime. 36 | openssl_cnf=" 37 | # This section is invoked when running 'openssl ca ...' 38 | [ ca ] 39 | default_ca = custom_ca_settings 40 | 41 | [ custom_ca_settings ] 42 | private_key = ${LOCAL_CA_KEY} 43 | certificate = ${LOCAL_CA_CRT} 44 | database = ${LOCAL_CA_DB} 45 | serial = ${LOCAL_CA_SRL} 46 | new_certs_dir = ${LOCAL_CA_CRT_DIR} 47 | default_days = 90 48 | default_md = sha256 49 | email_in_dn = yes 50 | unique_subject = no 51 | policy = custom_ca_policy 52 | 53 | [ custom_ca_policy ] 54 | countryName = optional 55 | stateOrProvinceName = optional 56 | localityName = optional 57 | organizationName = optional 58 | organizationalUnitName = optional 59 | commonName = supplied 60 | emailAddress = supplied 61 | 62 | # This section is invoked when running 'openssl req ...' 63 | [ req ] 64 | default_md = sha256 65 | prompt = no 66 | utf8 = yes 67 | string_mask = utf8only 68 | distinguished_name = dn_section 69 | # ^-- This needs to be defined else 'req' will fail with: 70 | # openssl unable to find 'distinguished_name' in config 71 | # If the '[dn_section]' is defined, but empty, we instead get: 72 | # error, no objects specified in config file 73 | # This is true even if we create a fully valid '-subj' string while using 74 | # these commands. LibreSSL also prioritize this content over what is being 75 | # sent in via '-subj', which is opposite to how OpenSSL works. Solution is 76 | # to assemble this section with the help of printf when using this command. 77 | 78 | # These extensions should be supplied when creating the CA certificate. 79 | [ ca_cert ] 80 | basicConstraints = critical, CA:true 81 | subjectKeyIdentifier = hash 82 | authorityKeyIdentifier = keyid:always,issuer:always 83 | keyUsage = critical, keyCertSign, cRLSign 84 | subjectAltName = email:copy 85 | issuerAltName = issuer:copy 86 | 87 | # These extensions should be supplied when creating a server certificate. 88 | [ server_cert ] 89 | basicConstraints = critical, CA:false 90 | subjectKeyIdentifier = hash 91 | authorityKeyIdentifier = keyid,issuer 92 | keyUsage = keyEncipherment, dataEncipherment, digitalSignature 93 | extendedKeyUsage = serverAuth, clientAuth 94 | issuerAltName = issuer:copy 95 | subjectAltName = @alt_names 96 | # ------------------------^ 97 | # Alt names must include all domain names/IPs the server certificate should be 98 | # valid for. This will be populated by the script later. 99 | " 100 | 101 | 102 | # Helper function to create a private key and a self-signed certificate to be 103 | # used as our local certificate authority. If the files already exist it will 104 | # do nothing, which means that it is actually possible to host mount a 105 | # completely custom CA here if you want to. 106 | generate_ca() { 107 | # Make sure necessary folders are present. 108 | mkdir_log "${LOCAL_CA_DIR}" 109 | mkdir_log "${LOCAL_CA_CRT_DIR}" 110 | 111 | # Make sure there is a private key available for the CA. 112 | if [ ! -f "${LOCAL_CA_KEY}" ]; then 113 | info "Generating new private key for local CA" 114 | openssl genrsa -out "${LOCAL_CA_KEY}" "${RSA_KEY_SIZE}" 115 | fi 116 | 117 | # Make sure there exists a self-signed certificate for the CA. 118 | if [ ! -f "${LOCAL_CA_CRT}" ]; then 119 | info "Creating new self-signed certificate for local CA" 120 | 121 | # We do allow changing the validity time of the locally generated CA, 122 | # but please read the advanced_usage.md#local-ca documentation before 123 | # doing this. 124 | if [ -z "${LOCAL_CA_ROOT_CERT_VALIDITY}" ]; then 125 | debug "LOCAL_CA_ROOT_CERT_VALIDITY unset, defaulting to 30 days" 126 | LOCAL_CA_ROOT_CERT_VALIDITY=30 127 | else 128 | info "LOCAL_CA_ROOT_CERT_VALIDITY set to custom value '${LOCAL_CA_ROOT_CERT_VALIDITY}'" 129 | fi 130 | 131 | openssl req -x509 -new -nodes \ 132 | -config <(printf "%s\n" \ 133 | "${openssl_cnf}" \ 134 | "[ dn_section ]" \ 135 | "countryName = SE" \ 136 | "0.organizationName = github.com/JonasAlfredsson" \ 137 | "organizationalUnitName = docker-nginx-certbot" \ 138 | "commonName = Local Debug CA" \ 139 | "emailAddress = ${CERTBOT_EMAIL}" \ 140 | ) \ 141 | -extensions ca_cert \ 142 | -days "${LOCAL_CA_ROOT_CERT_VALIDITY}" \ 143 | -key "${LOCAL_CA_KEY}" \ 144 | -out "${LOCAL_CA_CRT}" 145 | fi 146 | 147 | # If a serial file does not exist, or if it has a size of zero, we create 148 | # one with an initial value. 149 | if [ ! -f "${LOCAL_CA_SRL}" ] || [ ! -s "${LOCAL_CA_SRL}" ]; then 150 | info "Creating new serial file for local CA" 151 | openssl rand -hex 20 > "${LOCAL_CA_SRL}" 152 | fi 153 | 154 | # Make sure there is a database file. 155 | if [ ! -f "${LOCAL_CA_DB}" ]; then 156 | info "Creating new index file for local CA" 157 | touch "${LOCAL_CA_DB}" 158 | fi 159 | } 160 | 161 | # Helper function that use the local CA in order to create a valid signed 162 | # certificate for the given cert name. 163 | # 164 | # $1: The name of the certificate (e.g. domain) 165 | # $@: All alternate name variants, separated by space 166 | # (e.g. DNS.1=domain.org DNS.2=localhost IP.1=127.0.0.1) 167 | get_certificate() { 168 | # Store the cert name for future use, and then `shift` so the rest of the 169 | # input arguments are just alt names. 170 | local cert_name="$1" 171 | shift 172 | 173 | # Make sure the necessary folder exists. 174 | mkdir -vp "/etc/letsencrypt/live/${cert_name}" 175 | 176 | # Make sure there is a private key available for the domain in question. 177 | # It is good practice to generate a new key every time a new certificate is 178 | # requested, in order to guard against potential key compromises. 179 | info "Generating new private key for '${cert_name}'" 180 | openssl genrsa -out "/etc/letsencrypt/live/${cert_name}/privkey.pem" "${RSA_KEY_SIZE}" 181 | 182 | # Create a certificate signing request from the private key. 183 | info "Generating certificate signing request for '${cert_name}'" 184 | openssl req -new -config <(printf "%s\n" \ 185 | "${openssl_cnf}" \ 186 | "[ dn_section ]" \ 187 | "commonName = ${cert_name}" \ 188 | "emailAddress = ${CERTBOT_EMAIL}" \ 189 | ) \ 190 | -key "/etc/letsencrypt/live/${cert_name}/privkey.pem" \ 191 | -out "${LOCAL_CA_DIR}/${cert_name}.csr" 192 | 193 | # Sign the certificate with all the alternative names appended to the 194 | # appropriate section of the config file. 195 | info "Using local CA to sign certificate for '${cert_name}'" 196 | openssl ca -batch -notext \ 197 | -config <(printf "%s\n" \ 198 | "${openssl_cnf}" \ 199 | "[alt_names]" \ 200 | "$@" \ 201 | ) \ 202 | -extensions server_cert \ 203 | -in "${LOCAL_CA_DIR}/${cert_name}.csr" \ 204 | -out "/etc/letsencrypt/live/${cert_name}/cert.pem" 205 | 206 | # Create the other two files necessary to match what certbot produces. 207 | cp "${LOCAL_CA_CRT}" "/etc/letsencrypt/live/${cert_name}/chain.pem" 208 | cat "/etc/letsencrypt/live/${cert_name}/cert.pem" > "/etc/letsencrypt/live/${cert_name}/fullchain.pem" 209 | cat "/etc/letsencrypt/live/${cert_name}/chain.pem" >> "/etc/letsencrypt/live/${cert_name}/fullchain.pem" 210 | 211 | # Cleanup after ourselves. 212 | rm "${LOCAL_CA_DIR}/${cert_name}.csr" 213 | } 214 | 215 | # Begin with making sure that we have all the files necessary for a local CA. 216 | # This is really cheap to do, so I think it is fine that we check this every 217 | # time this script is invoked. 218 | generate_ca 219 | 220 | # Get all the cert names for which we should create certificates for, along 221 | # with the corresponding server names. 222 | # 223 | # This will return an associative array that looks something like this: 224 | # "cert_name" => "server_name1 server_name2" 225 | declare -A certificates 226 | while IFS= read -r -d $'\0' conf_file; do 227 | parse_config_file "${conf_file}" certificates 228 | done < <(find -L /etc/nginx/conf.d/ -name "*.conf*" -type f -print0) 229 | 230 | # Iterate over each key and create a signed certificate for them. 231 | for cert_name in "${!certificates[@]}"; do 232 | server_names=(${certificates["$cert_name"]}) 233 | 234 | # Assemble the list of domains to be included in the request. 235 | ip_count=0 236 | dns_count=0 237 | alt_names=() 238 | for server_name in "${server_names[@]}"; do 239 | if is_ip "${server_name}"; then 240 | # See if the alt name looks like an IP address. 241 | ip_count=$((${ip_count} + 1)) 242 | alt_names+=("IP.${ip_count}=${server_name}") 243 | else 244 | # Else we suppose this is a valid DNS name. 245 | dns_count=$((${dns_count} + 1)) 246 | alt_names+=("DNS.${dns_count}=${server_name}") 247 | fi 248 | done 249 | 250 | # Hand over all the info required for the certificate request, and 251 | # let the local CA handle the rest. 252 | if ! get_certificate "${cert_name}" "${alt_names[@]}"; then 253 | error "Local CA failed for '${cert_name}'. Check the logs for details." 254 | fi 255 | done 256 | 257 | # After trying to sign all of the certificates, auto enable any configs that we 258 | # did indeed succeed with. 259 | auto_enable_configs 260 | 261 | # Make sure the Nginx configs are valid. 262 | if ! nginx -t; then 263 | error "Nginx configuration is invalid, skipped reloading. Check the logs for details." 264 | exit 0 265 | fi 266 | 267 | # Finally, tell Nginx to reload the configs. 268 | nginx -s reload 269 | -------------------------------------------------------------------------------- /src/scripts/start_nginx_certbot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Helper function to gracefully shut down our child processes when we exit. 4 | clean_exit() { 5 | for PID in "${NGINX_PID}" "${CERTBOT_LOOP_PID}"; do 6 | if kill -0 "${PID}" 2>/dev/null; then 7 | kill -SIGTERM "${PID}" 8 | wait "${PID}" 9 | fi 10 | done 11 | } 12 | 13 | # Make bash listen to the SIGTERM, SIGINT and SIGQUIT kill signals, and make 14 | # them trigger a normal "exit" command in this script. Then we tell bash to 15 | # execute the "clean_exit" function, seen above, in the case an "exit" command 16 | # is triggered. This is done to give the child processes a chance to exit 17 | # gracefully. 18 | trap "exit" TERM INT QUIT 19 | trap "clean_exit" EXIT 20 | 21 | # Source "util.sh" so we can have our nice tools. 22 | . "$(cd "$(dirname "$0")"; pwd)/util.sh" 23 | 24 | # If the environment variable `DEBUG=1` is set, then this message is printed. 25 | debug "Debug messages are enabled" 26 | 27 | # Immediately symlink files to the correct locations and then run 28 | # 'auto_enable_configs' so that Nginx is in a runnable state 29 | # This will temporarily disable any misconfigured servers. 30 | symlink_user_configs 31 | auto_enable_configs 32 | 33 | # Start Nginx without its daemon mode (and save its PID). 34 | if [ 1 = "${DEBUG}" ]; then 35 | info "Starting the Nginx service in debug mode" 36 | nginx-debug -g "daemon off;" & 37 | NGINX_PID=$! 38 | else 39 | info "Starting the Nginx service" 40 | nginx -g "daemon off;" & 41 | NGINX_PID=$! 42 | fi 43 | debug "PID of the main Nginx process: ${NGINX_PID}" 44 | 45 | # Make sure a renewal interval is set before continuing. 46 | if [ -z "${RENEWAL_INTERVAL}" ]; then 47 | debug "RENEWAL_INTERVAL unset, using default of '8d'" 48 | RENEWAL_INTERVAL='8d' 49 | fi 50 | 51 | # Instead of trying to run 'cron' or something like that, just sleep and 52 | # call on certbot after the defined interval. 53 | ( 54 | set -e 55 | while true; do 56 | info "Running the autorenewal service" 57 | 58 | # Create symlinks from conf.d/ to user_conf.d/ if necessary. 59 | symlink_user_configs 60 | 61 | # Check that all dhparam files exists. 62 | "$(cd "$(dirname "$0")"; pwd)/create_dhparams.sh" 63 | 64 | if [ 1 = "${USE_LOCAL_CA}" ]; then 65 | # Renew all certificates with the help of the local CA. 66 | "$(cd "$(dirname "$0")"; pwd)/run_local_ca.sh" 67 | else 68 | # Run certbot to check if any certificates needs renewal. 69 | "$(cd "$(dirname "$0")"; pwd)/run_certbot.sh" 70 | fi 71 | 72 | # Finally we sleep for the defined time interval before checking the 73 | # certificates again. 74 | # The "if" statement afterwards is to enable us to terminate this sleep 75 | # process (via the HUP trap) without tripping the "set -e" setting. 76 | info "Autorenewal service will now sleep ${RENEWAL_INTERVAL}" 77 | sleep "${RENEWAL_INTERVAL}" || x=$?; if [ -n "${x}" ] && [ "${x}" -ne "143" ]; then exit "${x}"; fi 78 | done 79 | ) & 80 | CERTBOT_LOOP_PID=$! 81 | debug "PID of the autorenewal loop: ${CERTBOT_LOOP_PID}" 82 | 83 | # A helper function to prematurely terminate the sleep process, inside the 84 | # autorenewal loop process, in order to immediately restart the loop again 85 | # and thus reload any configuration files. 86 | reload_configs() { 87 | info "Received SIGHUP signal; terminating the autorenewal sleep process" 88 | if ! pkill -15 -P "${CERTBOT_LOOP_PID}" -fx "sleep ${RENEWAL_INTERVAL}"; then 89 | warning "No sleep process found, this most likely means that a renewal process is currently running" 90 | fi 91 | # On success we return 128 + SIGHUP in order to reduce the complexity of 92 | # the final wait loop. 93 | return 129 94 | } 95 | 96 | # Create a trap that listens to SIGHUP and runs the reloader function in case 97 | # such a signal is received. 98 | trap "reload_configs" HUP 99 | 100 | # Signal handler for SIGUSR1: forward it to nginx to reopen log files 101 | reopen_logs() { 102 | info "Received SIGUSR1 signal; telling nginx to reopen log files" 103 | nginx -s reopen 104 | return 138 # 128 + SIGUSR1 105 | } 106 | trap "reopen_logs" USR1 107 | 108 | # Nginx and the certbot update-loop process are now our children. As a parent 109 | # we will wait for both of their PIDs, and if one of them exits we will follow 110 | # suit and use the same status code as the program which exited first. 111 | # The loop is necessary since the signal traps will make any "wait" return 112 | # immediately when triggered, and to not exit the entire program we will have 113 | # to wait on the original PIDs again. 114 | while [ -z "${exit_code}" ] || [ "${exit_code}" = "129" ] || [ "${exit_code}" = "138" ]; do 115 | wait -n ${NGINX_PID} ${CERTBOT_LOOP_PID} 116 | exit_code=$? 117 | done 118 | 119 | debug "Exiting with code ${exit_code}" 120 | exit "${exit_code}" 121 | -------------------------------------------------------------------------------- /src/scripts/util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${DATE_FORMAT_STRING:="+%Y/%m/%d %T"} 4 | 5 | # Helper function used to output messages in a uniform manner. 6 | # 7 | # $1: The log level to print. 8 | # $2: The message to be printed. 9 | log() { 10 | echo "$(date "${DATE_FORMAT_STRING}") [${1}] ${2}" 11 | } 12 | 13 | # Helper function to output debug messages to STDOUT if the `DEBUG` environment 14 | # variable is set to 1. 15 | # 16 | # $1: String to be printed. 17 | debug() { 18 | if [ 1 = "${DEBUG}" ]; then 19 | log "debug" "${1}" 20 | fi 21 | } 22 | 23 | # Helper function to output informational messages to STDOUT. 24 | # 25 | # $1: String to be printed. 26 | info() { 27 | log "info" "${1}" 28 | } 29 | 30 | # Helper function to output warning messages to STDOUT, with bold yellow text. 31 | # 32 | # $1: String to be printed. 33 | warning() { 34 | (set +x; tput -Tscreen bold 35 | tput -Tscreen setaf 3 36 | log "warning" "${1}" 37 | tput -Tscreen sgr0) 38 | } 39 | 40 | # Helper function to output error messages to STDERR, with bold red text. 41 | # 42 | # $1: String to be printed. 43 | error() { 44 | (set +x; tput -Tscreen bold 45 | tput -Tscreen setaf 1 46 | log "error" "${1}" 47 | tput -Tscreen sgr0) >&2 48 | } 49 | 50 | # Helper function to print each folder created as a new DEBUG log line. 51 | # 52 | # $1: Directory to create. 53 | mkdir_log() { 54 | while IFS="" read -r line; do 55 | debug "${line}" 56 | done < <(mkdir -vp "${1}") 57 | } 58 | 59 | # Returns 0 if the parameter is an IPv4 or IPv6 address, 1 otherwise. 60 | # Can be used as `if is_ip "$something"; then`. 61 | # 62 | # $1: the parameter to check if it is an IP address. 63 | is_ip() { 64 | is_ipv4 "$1" || is_ipv6 "$1" 65 | } 66 | 67 | # Returns 0 if the parameter is an IPv4 address, 1 otherwise. 68 | # Can be used as `if is_ipv4 "$something"; then`. 69 | # 70 | # $1: the parameter to check if it is an IPv4 address. 71 | is_ipv4() { 72 | [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] 73 | } 74 | 75 | # Returns 0 if the parameter is an IPv6 address, 1 otherwise. 76 | # Can be used as `if is_ipv6 "$something"; then`. 77 | # 78 | # This comes from the amazing answer from David M. Syzdek 79 | # on stackoverflow: https://stackoverflow.com/a/17871737 80 | # 81 | # $1: the parameter to check if it is an IPv6 address. 82 | is_ipv6() { 83 | [[ "${1,,}" =~ ^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$ ]] 84 | } 85 | 86 | # Find lines that contain 'ssl_certificate_key', and try to extract a name from 87 | # each of these file paths. Each keyfile must be stored at the default location 88 | # of /etc/letsencrypt/live//privkey.pem, otherwise we ignore it since 89 | # it is most likely not a certificate that is managed by certbot. 90 | # 91 | # $1: Path to a Nginx configuration file. 92 | parse_cert_names() { 93 | sed -n -r -e 's&^\s*ssl_certificate_key\s+\/etc/letsencrypt/live/(.*)/privkey.pem;.*&\1&p' "$1" | xargs -n1 echo | uniq 94 | } 95 | 96 | # Nginx will answer to any domain name that is written on the line which starts 97 | # with 'server_name'. A server block may have multiple domain names defined on 98 | # this line, and a config file may have multiple server blocks. This method will 99 | # therefore try to extract all domain names and add them to the certificate 100 | # request being sent. Some things to think about: 101 | # * Wildcard names must use DNS authentication, else the challenge will fail. 102 | # * Possible overlappings. This method will find all 'server_names' in a .conf 103 | # file inside the conf.d/ folder and attach them to the request. If there are 104 | # different primary domains in the same .conf file it will cause some weird 105 | # certificates. Should however work fine but is not best practice. 106 | # * If the following comment "# certbot_domain:" is present 107 | # the end of the line it will be printed twice in such a fashion that it 108 | # encapsulate the server names that should be replaced with this one instead, 109 | # like this: 110 | # 1. certbot_domain:*.example.com 111 | # 2. certbot_domain:www.example.com 112 | # 3. certbot_domain:sub.example.com 113 | # 4. certbot_domain:*.example.com 114 | # * Unlike the other similar functions this one will not perform "uniq" on the 115 | # names, since that would prevent the feature explained above. 116 | # 117 | # $1: Path to a Nginx configuration file. 118 | parse_server_names() { 119 | sed -n -r -e 's&^\s*server_name\s+([^;]*);\s*#?(\s*certbot_domain:[^[:space:]]+)?.*$&\2 \1 \2&p' "$1" | xargs -n1 echo 120 | } 121 | 122 | # Return all unique "ssl_certificate_key" file paths. 123 | # 124 | # $1: Path to a Nginx configuration file. 125 | parse_keyfiles() { 126 | sed -n -r -e 's&^\s*ssl_certificate_key\s+(.*);.*&\1&p' "$1" | xargs -n1 echo | uniq 127 | } 128 | 129 | # Return all unique "ssl_certificate" file paths. 130 | # 131 | # $1: Path to a Nginx configuration file. 132 | parse_fullchains() { 133 | sed -n -r -e 's&^\s*ssl_certificate\s+(.*);.*&\1&p' "$1" | xargs -n1 echo | uniq 134 | } 135 | 136 | # Return all unique "ssl_trusted_certificate" file paths. 137 | # 138 | # $1: Path to a Nginx configuration file. 139 | parse_chains() { 140 | sed -n -r -e 's&^\s*ssl_trusted_certificate\s+(.*);.*&\1&p' "$1" | xargs -n1 echo | uniq 141 | } 142 | 143 | # Return all unique "dhparam" file paths. 144 | # 145 | # $1: Path to a Nginx configuration file. 146 | parse_dhparams() { 147 | sed -n -r -e 's&^\s*ssl_dhparam\s+(.*);.*&\1&p' "$1" | xargs -n1 echo | uniq 148 | } 149 | 150 | # Given a config file path, return 0 if all SSL related files exist (or there 151 | # are no files needed to be found). Return 1 otherwise (i.e. error exit code). 152 | # 153 | # This function calls the following functions in the specified order: 154 | # - parse_keyfiles 155 | # - parse_fullchains 156 | # - parse_chains 157 | # - parse_dhparams 158 | # 159 | # $1: Path to a Nginx configuration file. 160 | allfiles_exist() { 161 | local all_exist=0 162 | for type in keyfile fullchain chain dhparam; do 163 | for path in $(parse_"${type}"s "$1"); do 164 | if [[ "${path}" == data:* ]]; then 165 | debug "Ignoring ${type} path starting with 'data:' in '${1}'" 166 | elif [[ "${path}" == engine:* ]]; then 167 | debug "Ignoring ${type} path starting with 'engine:' in '${1}'" 168 | elif [ ! -s "${path}" ]; then 169 | warning "Could not find non-zero size ${type} file '${path}' in '${1}'" 170 | all_exist=1 171 | fi 172 | done 173 | done 174 | 175 | return ${all_exist} 176 | } 177 | 178 | # Parse the configuration file to find all the 'ssl_certificate_key' and the 179 | # 'server_name' entries, and aggregate the findings so a single certificate can 180 | # be ordered for multiple domains if this is desired. Each keyfile must be 181 | # stored in /etc/letsencrypt/live//privkey.pem, otherwise the 182 | # certificate/file will be ignored. 183 | # 184 | # If you are using the same associative array between each call to this function 185 | # it will make sure that only unique domain names are added to each specific 186 | # key. It will also ignore domain names that start with '~', since these are 187 | # regex and we cannot handle those. 188 | # 189 | # $1: The filepath to the configuration file. 190 | # $2: An associative bash array that will contain cert_name => server_names 191 | # (space-separated) after the call to this function. 192 | parse_config_file() { 193 | local conf_file=${1} 194 | local -n certs=${2} # Basically a pointer to the array sent in via $2. 195 | debug "Parsing config file '${conf_file}'" 196 | 197 | # Begin by checking if there are any certificates managed by us in the 198 | # config file. 199 | local cert_names=() 200 | for cert_name in $(parse_cert_names "${conf_file}"); do 201 | cert_names+=("${cert_name}") 202 | done 203 | if [ ${#cert_names[@]} -eq 0 ]; then 204 | debug "Found no valid certificate declarations in '${conf_file}'; skipping it" 205 | return 206 | fi 207 | 208 | # Then we look for all the possible server names present in the file. 209 | local server_names=() 210 | local replacement_domain="" 211 | for server_name in $(parse_server_names "${conf_file}"); do 212 | # Check if the current server_name line has a comment that tells us to 213 | # use a different domain name instead when making the request. 214 | if [[ "${server_name}" =~ certbot_domain:(.*) ]]; then 215 | if [ "${server_name}" == "certbot_domain:${replacement_domain}" ]; then 216 | # We found the end of the special server names. 217 | replacement_domain="" 218 | continue 219 | fi 220 | replacement_domain="${BASH_REMATCH[1]}" 221 | server_names+=("${replacement_domain}") 222 | continue 223 | fi 224 | if [ -n "${replacement_domain}" ]; then 225 | # Just continue in case we are substituting domains. 226 | debug "Substituting '${server_name}' with '${replacement_domain}'" 227 | continue 228 | fi 229 | 230 | # Ignore regex names, since these are not gracefully handled by this 231 | # code or certbot. 232 | if [[ "${server_name}" =~ ~(.*) ]]; then 233 | debug "Ignoring server name '${server_name}' since it looks like a regex and we cannot handle that" 234 | continue 235 | fi 236 | 237 | server_names+=("${server_name}") 238 | done 239 | debug "Found the following domain names: ${server_names[*]}" 240 | 241 | # Finally we add the found server names to the certificate names in 242 | # the associative array. 243 | for cert_name in "${cert_names[@]}"; do 244 | if ! [ ${certs["${cert_name}"]+_} ]; then 245 | debug "Adding new key '${cert_name}' in array" 246 | certs["${cert_name}"]="" 247 | else 248 | debug "Appending to already existing key '${cert_name}'" 249 | fi 250 | # Make sure we only add unique entries every time. 251 | # This invocation of awk works like 'sort -u', but preserves order. This 252 | # set the first 'server_name' entry as the first '-d' domain artgument 253 | # for the certbot command. This domain will be your Common Name on the 254 | # certificate. 255 | # stackoverflow on this awk usage: https://stackoverflow.com/a/45808487 256 | certs["${cert_name}"]="$(echo "${certs["${cert_name}"]}" "${server_names[@]}" | xargs -n1 echo | awk '!a[$0]++' | tr '\n' ' ')" 257 | done 258 | } 259 | 260 | # Creates symlinks from /etc/nginx/conf.d/ to all the files found inside 261 | # /etc/nginx/user_conf.d/. This will also remove broken links. 262 | symlink_user_configs() { 263 | debug "Creating symlinks to any files found in /etc/nginx/user_conf.d/" 264 | 265 | # Remove any broken symlinks that point back to the user_conf.d/ folder. 266 | while IFS= read -r -d $'\0' symlink; do 267 | info "Removing broken symlink '${symlink}' to '$(realpath "${symlink}")'" 268 | rm "${symlink}" 269 | done < <(find /etc/nginx/conf.d/ -xtype l -lname '/etc/nginx/user_conf.d/*' -print0) 270 | 271 | # Go through all files and directories in the user_conf.d/ folder and create 272 | # a symlink to them inside the conf.d/ folder. 273 | while IFS= read -r -d $'\0' source_file; do 274 | local symlinks_found=0 275 | 276 | # See if there already exist a symlink to this source file. 277 | while IFS= read -r -d $'\0' symlink; do 278 | debug "The file '${source_file}' is already symlinked by '${symlink}'" 279 | symlinks_found=$((symlinks_found + 1)) 280 | done < <(find -L /etc/nginx/conf.d/ -samefile "${source_file}" -print0) 281 | 282 | if [ "${symlinks_found}" -eq "1" ]; then 283 | # One symlink found, then we have nothing more to do. 284 | continue 285 | elif [ "${symlinks_found}" -gt "1" ]; then 286 | warning "Found more than one symlink to the file '${source_file}' inside '/etc/nginx/conf.d/'" 287 | continue 288 | fi 289 | 290 | # No symlinks to this file found, lets create one by just trimming the 291 | # known base folder path from the file and replacing it with our new 292 | # base file path. 293 | local link 294 | link="/etc/nginx/conf.d/${source_file#'/etc/nginx/user_conf.d/'}" 295 | info "Creating symlink '${link}' to '${source_file}'" 296 | mkdir_log "$(dirname -- "${link}")" 297 | ln -s "${source_file}" "${link}" 298 | done < <(find /etc/nginx/user_conf.d/ -type f -print0) 299 | } 300 | 301 | # Helper function that sifts through /etc/nginx/conf.d/, looking for configs 302 | # that don't have their necessary files yet, and disables them until everything 303 | # has been set up correctly. This also activates them afterwards. 304 | auto_enable_configs() { 305 | while IFS= read -r -d $'\0' conf_file; do 306 | if allfiles_exist "${conf_file}"; then 307 | if [ "${conf_file##*.}" = "nokey" ]; then 308 | info "Found all the necessary files for '${conf_file}', enabling..." 309 | mv "${conf_file}" "${conf_file%.*}" 310 | fi 311 | else 312 | if [ "${conf_file##*.}" = "conf" ]; then 313 | error "Important file(s) for '${conf_file}' are missing or empty, disabling..." 314 | mv "${conf_file}" "${conf_file}.nokey" 315 | fi 316 | fi 317 | done < <(find -L /etc/nginx/conf.d/ -name "*.conf*" -type f -print0) 318 | } 319 | -------------------------------------------------------------------------------- /tests/fixtures/ipv4_addresses.txt: -------------------------------------------------------------------------------- 1 | 192.168.42.42 2 | -------------------------------------------------------------------------------- /tests/fixtures/ipv6_addresses.txt: -------------------------------------------------------------------------------- 1 | 1:2:3:4:5:6:7:8 2 | 1:: 3 | 1:2:3:4:5:6:7:: 4 | 1::8 5 | 1:2:3:4:5:6::8 6 | 1:2:3:4:5:6::8 7 | 1::7:8 8 | 1:2:3:4:5::7:8 9 | 1:2:3:4:5::8 10 | 1::6:7:8 11 | 1:2:3:4::6:7:8 12 | 1:2:3:4::8 13 | 1::5:6:7:8 14 | 1:2:3::5:6:7:8 15 | 1:2:3::8 16 | 1::4:5:6:7:8 17 | 1:2::4:5:6:7:8 18 | 1:2::8 19 | 1::3:4:5:6:7:8 20 | 1::3:4:5:6:7:8 21 | 1::8 22 | ::2:3:4:5:6:7:8 23 | ::2:3:4:5:6:7:8 24 | ::8 25 | :: 26 | fe80::7:8%eth0 27 | fe80::7:8%1 28 | ::255.255.255.255 29 | ::ffff:255.255.255.255 30 | ::ffff:0:255.255.255.255 31 | 2001:db8:3:4::192.0.2.33 32 | 64:ff9b::192.0.2.33 33 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/multi_files/10-example.org.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org www.example.org; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert1/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert1/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert1/chain.pem; 8 | 9 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 10 | 11 | location / { 12 | alias /www-data/; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/multi_files/20-anew.example.org.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name anew.example.org; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert1/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert1/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert1/chain.pem; 8 | 9 | ssl_certificate /etc/letsencrypt/live/my-cert2/fullchain.pem; 10 | ssl_certificate_key /etc/letsencrypt/live/my-cert2/privkey.pem; 11 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert2/chain.pem; 12 | 13 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 14 | 15 | location / { 16 | alias /www-data/; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/multi_files/30-example.com.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.com *.example.com; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert2/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert2/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert2/chain.pem; 8 | 9 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 10 | 11 | location / { 12 | alias /www-data/; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/multi_files/40-example.net_1.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.net *.example.net www.example.net; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert3/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert3/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert3/chain.pem; 8 | 9 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 10 | 11 | location / { 12 | alias /www-data/; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/multi_files/40-example.net_2.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | # A lot of duplicates where everyone except the last one already exist. 4 | server_name example.net *.example.net; 5 | server_name example.net *.example.net www.example.net; 6 | server_name example.net www.example.net; 7 | server_name something.*; # certbot_domain:*.example.net 8 | server_name something-else.net; # certbot_domain:*.example.net 9 | server_name example.net ~.*.random.net; 10 | server_name example.net "~.*.whatever.net"; 11 | 12 | server_name new.example.net; 13 | 14 | ssl_certificate /etc/letsencrypt/live/my-cert3/fullchain.pem; 15 | ssl_certificate_key /etc/letsencrypt/live/my-cert3/privkey.pem; 16 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert3/chain.pem; 17 | 18 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 19 | 20 | location / { 21 | alias /www-data/; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/multi_certbot_domain_directive.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name sub.example.org www.example.org; # certbot_domain:*.example.org 4 | server_name example.org; # Other comment 5 | server_name sub.sub.example.org; # certbot_domain:*.sub.example.org 6 | 7 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 8 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 9 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 10 | 11 | include /etc/letsencrypt/options-ssl-nginx.conf; 12 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 13 | 14 | location / { 15 | alias /www-data/; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/multi_server_multi_cert_multi_name.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org www.example.org; 4 | server_name another.example.org; 5 | 6 | ssl_certificate /etc/letsencrypt/live/my-cert1/fullchain.pem; 7 | ssl_certificate_key /etc/letsencrypt/live/my-cert1/privkey.pem; 8 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert1/chain.pem; 9 | 10 | include /etc/letsencrypt/options-ssl-nginx.conf; 11 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 12 | 13 | location / { 14 | alias /www-data/; 15 | } 16 | } 17 | 18 | server { 19 | listen 443 ssl; 20 | server_name anew.example.org; 21 | 22 | ssl_certificate /etc/letsencrypt/live/my-cert1/fullchain.pem; 23 | ssl_certificate_key /etc/letsencrypt/live/my-cert1/privkey.pem; 24 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert1/chain.pem; 25 | 26 | ssl_certificate /etc/letsencrypt/live/my-cert2/fullchain.pem; 27 | ssl_certificate_key /etc/letsencrypt/live/my-cert2/privkey.pem; 28 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert2/chain.pem; 29 | 30 | include /etc/letsencrypt/options-ssl-nginx.conf; 31 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 32 | 33 | location / { 34 | alias /www-data/; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/multi_server_single_cert_single_name.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org www.example.org; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 8 | 9 | include /etc/letsencrypt/options-ssl-nginx.conf; 10 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 11 | 12 | location / { 13 | alias /www-data/; 14 | } 15 | } 16 | 17 | server { 18 | listen 443 ssl; 19 | server_name another.example.org; 20 | 21 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 22 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 23 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 24 | 25 | include /etc/letsencrypt/options-ssl-nginx.conf; 26 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 27 | 28 | location / { 29 | alias /www-data/; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/regex_server_names.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org; 4 | server_name ~^(?.+)\.example\.net$ www.example.org "~^(?\w\d{1,3}+)\.example\.net$"; 5 | server_name ~^(www\.)?(?.+)$; 6 | server_name _; 7 | server_name 192.168.0.1 1:2:3:4:5:6:7:8; 8 | 9 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 10 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 11 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 12 | 13 | include /etc/letsencrypt/options-ssl-nginx.conf; 14 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 15 | 16 | location / { 17 | alias /www-data/; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/single_certbot_domain_directive.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name sub.example.org www.example.org; # certbot_domain:*.example.org 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 8 | 9 | include /etc/letsencrypt/options-ssl-nginx.conf; 10 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 11 | 12 | location / { 13 | alias /www-data/; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/single_server_multi_cert_single_name.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org www.example.org; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert1/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert1/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert1/chain.pem; 8 | 9 | ssl_certificate /etc/letsencrypt/live/my-cert2/fullchain.pem; 10 | ssl_certificate_key /etc/letsencrypt/live/my-cert2/privkey.pem; 11 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert2/chain.pem; 12 | 13 | include /etc/letsencrypt/options-ssl-nginx.conf; 14 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 15 | 16 | location / { 17 | alias /www-data/; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/single_server_single_cert_multi_name.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org www.example.org; 4 | server_name another.example.org; 5 | 6 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 7 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 8 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 9 | 10 | include /etc/letsencrypt/options-ssl-nginx.conf; 11 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 12 | 13 | location / { 14 | alias /www-data/; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/nginx_config/single_files/single_server_single_cert_single_name.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name example.org www.example.org; 4 | 5 | ssl_certificate /etc/letsencrypt/live/my-cert/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/my-cert/privkey.pem; 7 | ssl_trusted_certificate /etc/letsencrypt/live/my-cert/chain.pem; 8 | 9 | include /etc/letsencrypt/options-ssl-nginx.conf; 10 | ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; 11 | 12 | location / { 13 | alias /www-data/; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/not_ip_addresses.txt: -------------------------------------------------------------------------------- 1 | example.org 2 | *.example.org 3 | -------------------------------------------------------------------------------- /tests/util.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | SCRIPTS_DIR="$(cd -- "${BATS_TEST_DIRNAME}/../src/scripts" &> /dev/null && pwd)" 4 | FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures" 5 | 6 | load "${SCRIPTS_DIR}/util.sh" 7 | 8 | 9 | @test "is_ipv4 detects what is an IPv6 address" { 10 | local ipv4addresses=($(<"${FIXTURES_DIR}/ipv4_addresses.txt")) 11 | 12 | for ipv4addr in ${ipv4addresses[@]}; do 13 | echo "Testing '$ipv4addr'" >&2 14 | is_ipv4 "$ipv4addr" 15 | done 16 | } 17 | 18 | @test "is_ipv4 detects what is not an IPv6 address" { 19 | local notipv4addresses=($(<"${FIXTURES_DIR}/not_ip_addresses.txt")) 20 | notipv4addresses+=($(<"${FIXTURES_DIR}/ipv6_addresses.txt")) 21 | 22 | for notipv4addr in ${notipv4addresses[@]}; do 23 | echo "Testing '$notipv4addr'" >&2 24 | ! is_ipv4 "$notipv4addr" 25 | done 26 | } 27 | 28 | @test "is_ipv6 detects what is an IPv6 address" { 29 | local ipv6addresses=($(<"${FIXTURES_DIR}/ipv6_addresses.txt")) 30 | 31 | for ipv6addr in ${ipv6addresses[@]}; do 32 | echo "Testing '$ipv6addr'" >&2 33 | is_ipv6 "$ipv6addr" 34 | done 35 | } 36 | 37 | @test "is_ipv6 detects what is not an IPv6 address" { 38 | local notipv6addresses=($(<"${FIXTURES_DIR}/not_ip_addresses.txt")) 39 | notipv6addresses+=($(<"${FIXTURES_DIR}/ipv4_addresses.txt")) 40 | 41 | for notipv6addr in ${notipv6addresses[@]}; do 42 | echo "Testing '$notipv6addr'" >&2 43 | ! is_ipv6 "$notipv6addr" 44 | done 45 | } 46 | 47 | @test "is_ip detects what is an IPv4 or an IPv6 address" { 48 | local ipaddresses=($(<"${FIXTURES_DIR}/ipv4_addresses.txt")) 49 | ipaddresses+=($(<"${FIXTURES_DIR}/ipv6_addresses.txt")) 50 | 51 | for ipaddr in ${ipaddresses[@]}; do 52 | echo "Testing '$ipaddr'" >&2 53 | is_ip "$ipaddr" 54 | done 55 | } 56 | 57 | @test "is_ip detects what is not an IPv4 or an IPv6 address" { 58 | local notipaddresses=($(<"${FIXTURES_DIR}/not_ip_addresses.txt")) 59 | 60 | for notipaddr in ${notipaddresses[@]}; do 61 | echo "Testing '$notipaddr'" >&2 62 | ! is_ip "$notipaddr" 63 | done 64 | } 65 | 66 | @test "parse_config_file works for single server block, single certificate name, single server name" { 67 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/single_server_single_cert_single_name.conf" 68 | 69 | local -A certificates 70 | parse_config_file "${fixture}" certificates 71 | local -p certificates 72 | 73 | [ ${#certificates[@]} -eq 1 ] 74 | [ -n "${certificates[my-cert]}" ] 75 | 76 | local server_names=(${certificates[my-cert]}) 77 | [ ${#server_names[@]} -eq 2 ] 78 | [ "${server_names[0]}" == "example.org" ] 79 | [ "${server_names[1]}" == "www.example.org" ] 80 | } 81 | 82 | @test "parse_config_file works for single server block, single certificate name, multiple server names" { 83 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/single_server_single_cert_multi_name.conf" 84 | 85 | local -A certificates 86 | parse_config_file "${fixture}" certificates 87 | 88 | [ ${#certificates[@]} -eq 1 ] 89 | [ -n "${certificates[my-cert]}" ] 90 | 91 | local server_names=(${certificates[my-cert]}) 92 | [ ${#server_names[@]} -eq 3 ] 93 | [ "${server_names[0]}" == "example.org" ] 94 | [ "${server_names[1]}" == "www.example.org" ] 95 | [ "${server_names[2]}" == "another.example.org" ] 96 | } 97 | 98 | @test "parse_config_file works for single server block, multiple certificate names, single server name" { 99 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/single_server_multi_cert_single_name.conf" 100 | 101 | local -A certificates 102 | parse_config_file "${fixture}" certificates 103 | local -p certificates 104 | 105 | [ ${#certificates[@]} -eq 2 ] 106 | [ -n "${certificates[my-cert1]}" ] 107 | [ -n "${certificates[my-cert2]}" ] 108 | 109 | local server_names_cert1=(${certificates[my-cert1]}) 110 | [ ${#server_names_cert1[@]} -eq 2 ] 111 | [ "${server_names_cert1[0]}" == "example.org" ] 112 | [ "${server_names_cert1[1]}" == "www.example.org" ] 113 | 114 | local server_names_cert2=(${certificates[my-cert2]}) 115 | [ ${#server_names_cert2[@]} -eq 2 ] 116 | [ "${server_names_cert2[0]}" == "example.org" ] 117 | [ "${server_names_cert2[1]}" == "www.example.org" ] 118 | } 119 | 120 | @test "parse_config_file works for multiple server blocks, single certificate name, single server name" { 121 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/multi_server_single_cert_single_name.conf" 122 | 123 | local -A certificates 124 | parse_config_file "${fixture}" certificates 125 | local -p certificates 126 | 127 | [ ${#certificates[@]} -eq 1 ] 128 | [ -n "${certificates[my-cert]}" ] 129 | 130 | local server_names=(${certificates[my-cert]}) 131 | [ ${#server_names[@]} -eq 3 ] 132 | [ "${server_names[0]}" == "example.org" ] 133 | [ "${server_names[1]}" == "www.example.org" ] 134 | [ "${server_names[2]}" == "another.example.org" ] 135 | } 136 | 137 | @test "parse_config_file works for multiple server blocks, multiple certificate names, multiple server names" { 138 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/multi_server_multi_cert_multi_name.conf" 139 | 140 | local -A certificates 141 | parse_config_file "${fixture}" certificates 142 | local -p certificates 143 | 144 | [ ${#certificates[@]} -eq 2 ] 145 | [ -n "${certificates[my-cert1]}" ] 146 | [ -n "${certificates[my-cert2]}" ] 147 | 148 | local server_names_cert1=(${certificates[my-cert1]}) 149 | [ ${#server_names_cert1[@]} -eq 4 ] 150 | [ "${server_names_cert1[0]}" == "example.org" ] 151 | [ "${server_names_cert1[1]}" == "www.example.org" ] 152 | [ "${server_names_cert1[2]}" == "another.example.org" ] 153 | [ "${server_names_cert1[3]}" == "anew.example.org" ] 154 | 155 | local server_names_cert2=(${certificates[my-cert2]}) 156 | [ ${#server_names_cert2[@]} -eq 4 ] 157 | [ "${server_names_cert2[0]}" == "example.org" ] 158 | [ "${server_names_cert2[1]}" == "www.example.org" ] 159 | [ "${server_names_cert2[2]}" == "another.example.org" ] 160 | [ "${server_names_cert2[3]}" == "anew.example.org" ] 161 | 162 | } 163 | 164 | @test "parse_config_file supports a single certbot_domain directive" { 165 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/single_certbot_domain_directive.conf" 166 | 167 | local -A certificates 168 | parse_config_file "${fixture}" certificates 169 | local -p certificates 170 | 171 | [ ${#certificates[@]} -eq 1 ] 172 | [ -n "${certificates[my-cert]}" ] 173 | 174 | local server_names=(${certificates[my-cert]}) 175 | [ ${#server_names[@]} -eq 1 ] 176 | [ "${server_names[0]}" == "*.example.org" ] 177 | } 178 | 179 | @test "parse_config_file supports multiple certbot_domain directives" { 180 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/multi_certbot_domain_directive.conf" 181 | 182 | local -A certificates 183 | parse_config_file "${fixture}" certificates 184 | local -p certificates 185 | 186 | [ ${#certificates[@]} -eq 1 ] 187 | [ -n "${certificates[my-cert]}" ] 188 | 189 | local server_names=(${certificates[my-cert]}) 190 | [ ${#server_names[@]} -eq 3 ] 191 | [ "${server_names[0]}" == "*.example.org" ] 192 | [ "${server_names[1]}" == "example.org" ] 193 | [ "${server_names[2]}" == "*.sub.example.org" ] 194 | } 195 | 196 | @test "parse_config_file ignores regex names" { 197 | local fixture="${FIXTURES_DIR}/nginx_config/single_files/regex_server_names.conf" 198 | 199 | local -A certificates 200 | parse_config_file "${fixture}" certificates 201 | local -p certificates 202 | 203 | [ ${#certificates[@]} -eq 1 ] 204 | [ -n "${certificates[my-cert]}" ] 205 | 206 | local server_names=(${certificates[my-cert]}) 207 | echo "${certificates[@]}" 208 | [ ${#server_names[@]} -eq 5 ] 209 | [ "${server_names[0]}" == "example.org" ] 210 | [ "${server_names[1]}" == "www.example.org" ] 211 | [ "${server_names[2]}" == "_" ] 212 | [ "${server_names[3]}" == "192.168.0.1" ] 213 | [ "${server_names[4]}" == "1:2:3:4:5:6:7:8" ] 214 | } 215 | 216 | @test "parse_config_file works over multiple files (with duplicates)" { 217 | local -A certificates 218 | for conf_file in ${FIXTURES_DIR}/nginx_config/multi_files/*.conf*; do 219 | parse_config_file "${conf_file}" certificates 220 | done 221 | 222 | local -p certificates 223 | [ ${#certificates[@]} -eq 3 ] 224 | [ -n "${certificates[my-cert1]}" ] 225 | [ -n "${certificates[my-cert2]}" ] 226 | [ -n "${certificates[my-cert3]}" ] 227 | 228 | local server_names_cert1=(${certificates[my-cert1]}) 229 | [ ${#server_names_cert1[@]} -eq 3 ] 230 | [ "${server_names_cert1[0]}" == "example.org" ] 231 | [ "${server_names_cert1[1]}" == "www.example.org" ] 232 | [ "${server_names_cert1[2]}" == "anew.example.org" ] 233 | 234 | local server_names_cert2=(${certificates[my-cert2]}) 235 | [ ${#server_names_cert2[@]} -eq 3 ] 236 | [ "${server_names_cert2[0]}" == "anew.example.org" ] 237 | [ "${server_names_cert2[1]}" == "example.com" ] 238 | [ "${server_names_cert2[2]}" == "*.example.com" ] 239 | 240 | local server_names_cert3=(${certificates[my-cert3]}) 241 | [ ${#server_names_cert3[@]} -eq 4 ] 242 | [ "${server_names_cert3[0]}" == "example.net" ] 243 | [ "${server_names_cert3[1]}" == "*.example.net" ] 244 | [ "${server_names_cert3[2]}" == "www.example.net" ] 245 | [ "${server_names_cert3[3]}" == "new.example.net" ] 246 | } 247 | --------------------------------------------------------------------------------