diff --git a/nginx b/nginx deleted file mode 160000 index 62f3ca7..0000000 --- a/nginx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 62f3ca752b2a9dfa3f446a77fde0b062e676abf2 diff --git a/nginx/.env b/nginx/.env new file mode 100644 index 0000000..28b1fa2 --- /dev/null +++ b/nginx/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=evgeniy-khyst diff --git a/nginx/LICENSE b/nginx/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/nginx/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/nginx/README.md b/nginx/README.md new file mode 100644 index 0000000..a876866 --- /dev/null +++ b/nginx/README.md @@ -0,0 +1,356 @@ +# Nginx and Let’s Encrypt with Docker Compose in less than 3 minutes + +- [Overview](#3b878279a04dc47d60932cb294d96259) +- [Initial setup](#1231369e1218613623e1b520c27ce190) + - [Prerequisites](#ee68e5b99222bbc29a480fcb0d1d6ee2) + - [Step 0 - Create DNS records](#288c0835566de0a785d19451eac904a0) + - [Step 1 - Edit domain names and emails in the configuration](#f24b6b41d1afb4cf65b765cf05a44ac1) + - [Step 2 - Configure Nginx virtual hosts](#3414177b596079dbf39b1b7fa10234c6) + - [Serving static content](#cdbe8e85146b30abdbb3425163a3b7a2) + - [Proxying all requests to a backend server](#c156f4dfc046a4229590da3484f9478d) + - [Step 3 - Create named Docker volumes for dummy and Let's Encrypt TLS certificates](#b56e2fee036d09a35898559d9889bae7) + - [Step 4 - Build images and start containers using staging Let's Encrypt server](#4952d0670f6fb00a0337d2251621508a) + - [Step 5 - verify HTTPS works with the staging certificates](#46d3804a4859874ba8b6ced6013b9966) + - [Step 6 - Switch to production Let's Encrypt server](#04529d361bbd6586ebcf267da5f0dfd7) + - [Step 7 - verify HTTPS works with the production certificates](#70d8ba04ba9117ff3ba72a9413131351) +- [Reloading Nginx configuration without downtime](#45a36b34f024f33bed82349e9096051a) +- [Adding a new domain to a running solution](#35a7ab6c3c12c73a0fce287690b1c216) + - [Step 0 - Create a new DNS records](#22e1d8b6115f1b1aaf65d61ee2557e52) + - [Step 1 - Add domain name and email to the configuration](#d0a4d4424e2e96c4dbe1a28dfddf7224) + - [Step 2 - Configure a new Nginx virtual hosts](#96dc528b7365f5a119bb2b1893f60700) + - [Step 3 - Restart Docker containers](#38f75935bf20b547d1f6788791645d5d) +- [Directory structure](#7cd115332ea5785828a7a0b5249f0755) +- [Configuration file structure](#bcd6f4d91c9b46c9af4d5b8c4a07db77) +- [SSL configuration for A+ rating](#f9987558925ac3a1ca42e184e10d7b73) +- [Removing a domain name from a running solution](#90d955c4-2684-11ed-a261-0242ac120002) + - [Step 1 - Remove the .conf file](#90d9588a-2684-11ed-a261-0242ac120002) + - [Step 2 - Remove domain name](#90d959b6-2684-11ed-a261-0242ac120002) + - [Step 3 - Update Docker containers](#90d95ace-2684-11ed-a261-0242ac120002) + + + +## Overview + +This example automatically obtains and renews [Let's Encrypt](https://letsencrypt.org/) TLS certificates and sets up HTTPS in Nginx for multiple domain names using Docker Compose. + +You can set up HTTPS in Nginx with Let's Encrypt TLS certificates for your domain names and get an A+ rating in [SSL Labs SSL Server Test](https://www.ssllabs.com/ssltest/) by changing a few configuration parameters of this example. + +Let's Encrypt is a certificate authority that provides free X.509 certificates for TLS encryption. +The certificates are valid for 90 days and can be renewed. Both initial creation and renewal can be automated using [Certbot](https://certbot.eff.org/). + +When using Kubernetes Let's Encrypt TLS certificates can be easily obtained and installed using [Cert Manager](https://cert-manager.io/). +For simple websites and applications, Kubernetes is too much overhead and Docker Compose is more suitable. +But for Docker Compose there is no such popular and robust tool for TLS certificate management. + +The example supports separate TLS certificates for multiple domain names, e.g. `example.com`, `anotherdomain.net` etc. +For simplicity this example deals with the following domain names: + +- `test1.evgeniy-khyst.com` +- `test2.evgeniy-khyst.com` + +The idea is simple. There are 3 containers: + +- **Nginx** +- **Certbot** - for obtaining and renewing certificates +- **Cron** - for triggering certificates renewal once a day + +The sequence of actions: + +1. Nginx generates self-signed "dummy" certificates to pass ACME challenge for obtaining Let's Encrypt certificates +2. Certbot waits for Nginx to become ready and obtains certificates +3. Cron triggers Certbot to try to renew certificates and Nginx to reload configuration daily + +## Initial setup + +### Prerequisites + +1. [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed +2. You have a domain name +3. You have a server with a publicly routable IP address +4. You have cloned this repository (or created and cloned a [fork](https://github.com/evgeniy-khist/letsencrypt-docker-compose/fork)): + ```bash + git clone https://github.com/evgeniy-khist/letsencrypt-docker-compose.git + ``` + +### Step 0 - Create DNS records + +For all domain names create DNS A records to point to a server where Docker containers will be running. +Also, consider creating CNAME records for the `www` subdomains. + +**DNS records** + +| Type | Hostname | Value | +| ----- | ----------------------------- | ---------------------------------------- | +| A | `test1.evgeniy-khyst.com` | directs to IP address `X.X.X.X` | +| A | `test2.evgeniy-khyst.com` | directs to IP address `X.X.X.X` | +| CNAME | `www.test1.evgeniy-khyst.com` | is an alias of `test1.evgeniy-khyst.com` | +| CNAME | `www.test2.evgeniy-khyst.com` | is an alias of `test2.evgeniy-khyst.com` | + +### Step 1 - Edit domain names and emails in the configuration + +Specify your domain names and contact emails for these domains with space as delimiter in the [`config.env`](config.env): + +```bash +DOMAINS="test1.evgeniy-khyst.com test2.evgeniy-khyst.com" +CERTBOT_EMAILS="info@evgeniy-khyst.com info@evgeniy-khyst.com" +``` + +For two and more domains separated by space use double quotes (`"`) around the `DOMAINS` and `CERTBOT_EMAILS` variables. + +For a single domain double quotes can be omitted: + +```bash +DOMAINS=test1.evgeniy-khyst.com +CERTBOT_EMAILS=info@evgeniy-khyst.com +``` + +### Step 2 - Configure Nginx virtual hosts + +For each domain configure the Nginx [`server` block](https://nginx.org/en/docs/http/ngx_http_core_module.html#server) by updating `vhosts/${domain}.conf`: + +- `vhosts/test1.evgeniy-khyst.com.conf` +- `vhosts/test2.evgeniy-khyst.com.conf` + +#### Serving static content + +``` +location / { + root /var/www/html/my-domain; + index index.html index.htm; +} +``` + +Make sure `html/my-domain` directory (relative to the repository root) exists and countains the desired content and `html` directory is mounted as `/var/www/html` in `docker-compose.yml`: + +```yaml +services: + nginx: + #... + volumes: + #... + - ./html:/var/www/html +``` + +#### Proxying all requests to a backend server + +``` +location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://my-backend:8080/; +} +``` + +`my-backend` is the service name of your backend application in `docker-compose.yml`: + +```yaml +services: + my-backend: + image: example.com/my-backend:1.0.0 + #... + ports: + - "8080" +``` + +### Step 3 - Create named Docker volumes for dummy and Let's Encrypt TLS certificates + +```bash +docker volume create --name=nginx_conf +docker volume create --name=letsencrypt_certs +``` + +### Step 4 - Build images and start containers using staging Let's Encrypt server + +```bash +docker compose up -d --build +docker compose logs -f +``` + +You can alternatively use the `docker-compose` binary. + +For each domain wait for the following log messages: + +``` +Switching Nginx to use Let's Encrypt certificate +Reloading Nginx configuration +``` + +### Step 5 - verify HTTPS works with the staging certificates + +For each domain open in browser `https://${domain}` and `https://www.${domain}` and verify that staging Let's Encrypt certificates are working: + +- https://test1.evgeniy-khyst.com, https://www.test1.evgeniy-khyst.com +- https://test2.evgeniy-khyst.com, https://www.test2.evgeniy-khyst.com + +Certificates issued by `(STAGING) Let's Encrypt` are considered not secure by browsers. + +### Step 6 - Switch to production Let's Encrypt server + +Stop the containers: + +```bash +docker compose down +``` + +Configure to use production Let's Encrypt server in [`config.env`](config.env): + +```properties +CERTBOT_TEST_CERT=0 +``` + +Re-create the volume for Let's Encrypt certificates: + +```bash +docker volume rm letsencrypt_certs +docker volume create --name=letsencrypt_certs +``` + +Start the containers: + +```bash +docker compose up -d +docker compose logs -f +``` + +### Step 7 - verify HTTPS works with the production certificates + +For each domain open in browser `https://${domain}` and `https://www.${domain}` and verify that production Let's Encrypt certificates are working. + +Certificates issued by `Let's Encrypt` are considered secure by browsers. + +Optionally check your domains with [SSL Labs SSL Server Test](https://www.ssllabs.com/ssltest/) and review the SSL Reports. + +## Reloading Nginx configuration without downtime + +Update a configuration in `vhosts/${domain}.conf`. + +Do a hot reload of the Nginx configuration: + +```bash +docker compose exec --no-TTY nginx nginx -s reload +``` + +## Adding a new domain to a running solution + +Let's add a third domain `test3.evgeniy-khyst.com` to a running solution. + +### Step 0 - Create a new DNS records + +Create DNS A record and CNAME record for `www` subdomain. + +**DNS records** + +| Type | Hostname | Value | +| ----- | ----------------------------- | ---------------------------------------- | +| A | `test3.evgeniy-khyst.com` | directs to IP address `X.X.X.X` | +| CNAME | `www.test3.evgeniy-khyst.com` | is an alias of `test3.evgeniy-khyst.com` | + +### Step 1 - Add domain name and email to the configuration + +Add a new domain name (`test3.evgeniy-khyst.com`) and contact email to the [`config.env`](config.env): + +```properties +DOMAINS="test1.evgeniy-khyst.com test2.evgeniy-khyst.com test3.evgeniy-khyst.com" +CERTBOT_EMAILS="info@evgeniy-khyst.com info@evgeniy-khyst.com info@evgeniy-khyst.com" +``` + +### Step 2 - Configure a new Nginx virtual hosts + +Create a virtual host configuration file `vhosts/test3.evgeniy-khyst.com.conf` for the new domain. + +For example, for serving static content use the following configuration: + +``` +location / { + root /var/www/html/test3.evgeniy-khyst.com; + index index.html index.htm; +} +``` + +Create a webroot `html/test3.evgeniy-khyst.com` and add static content. + +### Step 3 - Restart Docker containers + +```bash +docker compose down +docker compose up -d +docker compose logs -f +``` + +## Directory structure + +- [`docker-compose.yml`](docker-compose.yml) +- [`.env`](.env) - specifies `COMPOSE_PROJECT_NAME` to make container names independent from the base directory name +- [`config.env`](config.env) - specifies project configuration, e.g. domain names, emails etc. +- [`nginx/`](nginx/) + - [`Dockerfile`](nginx/Dockerfile) + - [`nginx.sh`](nginx/nginx.sh) - entrypoint script + - [`default.conf`](nginx/default.conf) - common settings for all domains. The file is copied to `/etc/nginx/conf.d/` + - [`gzip.conf`](nginx/gzip.conf) - Gzip compression. Included in `default.conf` + - [`site.conf.tpl`](nginx/site.conf.tpl) - virtual host configuration template used to create configuration files `/etc/nginx/sites/${domain}.conf` included in `default.conf` + - [`options-ssl-nginx.conf`](nginx/options-ssl-nginx.conf) - a configuration to get A+ rating at [SSL Server Test](https://www.ssllabs.com/ssltest/). Included in `site.conf.tpl` + - [`hsts.conf`](nginx/hsts.conf) - HTTP Strict Transport Security (HSTS) policy. Included in `site.conf.tpl` +- [`vhosts/`](vhosts/) + - [`test1.evgeniy-khyst.com.conf`](vhosts/test1.evgeniy-khyst.com.conf) - `server` block configuration for serving static content. Included in `site.conf.tpl` (`include /etc/nginx/vhosts/${domain}.conf;`) + - [`test2.evgeniy-khyst.com.conf`](vhosts/test2.evgeniy-khyst.com.conf) - `server` block configuration for serving static content. Included in `site.conf.tpl` (`include /etc/nginx/vhosts/${domain}.conf;`) +- [`html/`](html/) + - [`test1.evgeniy-khyst.com/`](html/test1.evgeniy-khyst.com/) - directory mounted as a webroot for `test1.evgeniy-khyst.com`. Configured in `vhosts/test1.evgeniy-khyst.com.conf` + - [`index.html`](html/test1.evgeniy-khyst.com/index.html) + - [`test2.evgeniy-khyst.com/`](html/test2.evgeniy-khyst.com/) - directory mounted as a webroot for `test2.evgeniy-khyst.com`. Configured in `vhosts/test2.evgeniy-khyst.com.conf` + - [`index.html`](html/test2.evgeniy-khyst.com/index.html) +- [`certbot/`](certbot/) + - [`Dockerfile`](certbot/Dockerfile) + - [`certbot.sh`](certbot/certbot.sh) - entrypoint script +- [`cron/`](cron/) + - [`Dockerfile`](cron/Dockerfile) + - [`renew_certs.sh`](cron/renew_certs.sh) - script executed on a daily basis to try to renew certificates + +## Configuration file structure + +To adapt the example to your domain names you need to change only [`config.env`](config.env): + +```properties +DOMAINS="test1.evgeniy-khyst.com test2.evgeniy-khyst.com" +CERTBOT_EMAILS="info@evgeniy-khyst.com info@evgeniy-khyst.com" +CERTBOT_TEST_CERT=1 +CERTBOT_RSA_KEY_SIZE=4096 +``` + +Configuration parameters: + +- `DOMAINS` - a space separated list of domains to manage certificates for +- `CERTBOT_EMAILS` - a space separated list of email for corresponding domains. If not specified, certificates will be obtained with `--register-unsafely-without-email` +- `CERTBOT_TEST_CERT` - use Let's Encrypt staging server (`--test-cert`) + +Let's Encrypt has rate limits. So, while testing it's better to use staging server by setting `CERTBOT_TEST_CERT=1` (default value). +When you are ready to use production Let's Encrypt server, set `CERTBOT_TEST_CERT=0`. + +## SSL configuration for A+ rating + +SSL in Nginx is configured accoring to best practices to get A+ rating in [SSL Labs SSL Server Test](https://www.ssllabs.com/ssltest/). + +Read more about the best practices and rating: + +- https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices +- https://github.com/ssllabs/research/wiki/SSL-Server-Rating-Guide + +## Removing a domain name from a running solution + +### Step 1 - Remove the `.conf` file + +Remove the `domain.to.remove.com.conf` file from `vhost` + +### Step 2 - Remove domain name + +Remove the domain name from [`config.env`](config.env) + +### Step 3 - Update Docker containers + +```bash +docker compose down +docker volume rm nginx_conf +docker volume create --name=nginx_conf +docker compose up -d +``` diff --git a/nginx/certbot/Dockerfile b/nginx/certbot/Dockerfile new file mode 100644 index 0000000..10fd1fe --- /dev/null +++ b/nginx/certbot/Dockerfile @@ -0,0 +1,9 @@ +FROM certbot/certbot:v1.29.0 + +RUN apk add --no-cache bash + +COPY certbot.sh /opt/ + +RUN chmod +x /opt/certbot.sh + +ENTRYPOINT ["/opt/certbot.sh"] \ No newline at end of file diff --git a/nginx/certbot/certbot.sh b/nginx/certbot/certbot.sh new file mode 100644 index 0000000..6e672fb --- /dev/null +++ b/nginx/certbot/certbot.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e + +trap exit INT TERM + +if [ -z "$DOMAINS" ]; then + echo "DOMAINS environment variable is not set" + exit 1; +fi + +until nc -z nginx 80; do + echo "Waiting for nginx to start..." + sleep 5s & wait ${!} +done + +if [ "$CERTBOT_TEST_CERT" != "0" ]; then + test_cert_arg="--test-cert" +fi + +domains_fixed=$(echo "$DOMAINS" | tr -d \") +domain_list=($domains_fixed) +emails_fixed=$(echo "$CERTBOT_EMAILS" | tr -d \") +emails_list=($emails_fixed) +for i in "${!domain_list[@]}"; do + domain="${domain_list[i]}" + + mkdir -p "/var/www/certbot/$domain" + + if [ -d "/etc/letsencrypt/live/$domain" ]; then + echo "Let's Encrypt certificate for $domain already exists" + continue + fi + + email="${emails_list[i]}" + if [ -z "$email" ]; then + email_arg="--register-unsafely-without-email" + echo "Obtaining the certificate for $domain without email" + else + email_arg="--email $email" + echo "Obtaining the certificate for $domain with email $email" + fi + + certbot certonly \ + --webroot \ + -w "/var/www/certbot/$domain" \ + -d "$domain" -d "www.$domain" \ + $test_cert_arg \ + $email_arg \ + --rsa-key-size "${CERTBOT_RSA_KEY_SIZE:-4096}" \ + --agree-tos \ + --noninteractive \ + --verbose || true +done diff --git a/nginx/config.env b/nginx/config.env new file mode 100644 index 0000000..6f3d84c --- /dev/null +++ b/nginx/config.env @@ -0,0 +1,4 @@ +DOMAINS="felia.hwtr.dev" +CERTBOT_EMAILS="pegasucksgg@gmail.com" +CERTBOT_TEST_CERT=1 +CERTBOT_RSA_KEY_SIZE=4096 diff --git a/nginx/cron/Dockerfile b/nginx/cron/Dockerfile new file mode 100644 index 0000000..1b73b3e --- /dev/null +++ b/nginx/cron/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3.16 + +RUN apk update && \ + apk add --no-cache docker-cli docker-cli-compose + +COPY renew_certs.sh /etc/periodic/daily/renew_certs + +RUN chmod +x /etc/periodic/daily/renew_certs + +WORKDIR /workdir + +CMD ["crond", "-f", "-l", "0"] diff --git a/nginx/cron/renew_certs.sh b/nginx/cron/renew_certs.sh new file mode 100644 index 0000000..267dd93 --- /dev/null +++ b/nginx/cron/renew_certs.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +cd /workdir +echo "Renewing Let's Encrypt Certificates... (`date`)" +docker compose run --rm --no-TTY --entrypoint certbot certbot renew --no-random-sleep-on-renew +echo "Reloading Nginx configuration" +docker compose exec --no-TTY nginx nginx -s reload diff --git a/nginx/docker-compose.yml b/nginx/docker-compose.yml new file mode 100644 index 0000000..d61569f --- /dev/null +++ b/nginx/docker-compose.yml @@ -0,0 +1,43 @@ +version: "2" + +services: + nginx: + build: ./nginx + image: evgeniy-khyst/nginx + env_file: + - ./config.env + volumes: + - nginx_conf:/etc/nginx/sites + - letsencrypt_certs:/etc/letsencrypt + - certbot_acme_challenge:/var/www/certbot + - ./vhosts:/etc/nginx/vhosts + - ./html:/var/www/html + ports: + - "80:80" + - "443:443" + restart: unless-stopped + + certbot: + build: ./certbot + image: evgeniy-khyst/certbot + env_file: + - ./config.env + volumes: + - letsencrypt_certs:/etc/letsencrypt + - certbot_acme_challenge:/var/www/certbot + + cron: + build: ./cron + image: evgeniy-khyst/cron + environment: + COMPOSE_PROJECT_NAME: "${COMPOSE_PROJECT_NAME}" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./:/workdir:ro + restart: unless-stopped + +volumes: + nginx_conf: + letsencrypt_certs: + certbot_acme_challenge: + diff --git a/nginx/html/felia.hwtr.dev/index.html b/nginx/html/felia.hwtr.dev/index.html new file mode 100644 index 0000000..beaa597 --- /dev/null +++ b/nginx/html/felia.hwtr.dev/index.html @@ -0,0 +1 @@ +hello felia diff --git a/nginx/nginx/Dockerfile b/nginx/nginx/Dockerfile new file mode 100644 index 0000000..f413487 --- /dev/null +++ b/nginx/nginx/Dockerfile @@ -0,0 +1,14 @@ +FROM nginx:1.23-alpine + +RUN apk add --no-cache openssl + +COPY default.conf /etc/nginx/conf.d/ +COPY gzip.conf options-ssl-nginx.conf hsts.conf /etc/nginx/includes/ +COPY site.conf.tpl /customization/ +COPY nginx.sh /customization/ + +RUN chmod +x /customization/nginx.sh + +EXPOSE 80 + +CMD ["/customization/nginx.sh"] \ No newline at end of file diff --git a/nginx/nginx/default.conf b/nginx/nginx/default.conf new file mode 100644 index 0000000..4d755ad --- /dev/null +++ b/nginx/nginx/default.conf @@ -0,0 +1,5 @@ +server_names_hash_bucket_size 64; + +include /etc/nginx/includes/gzip.conf; + +include /etc/nginx/sites/*.conf; diff --git a/nginx/nginx/gzip.conf b/nginx/nginx/gzip.conf new file mode 100644 index 0000000..99fa776 --- /dev/null +++ b/nginx/nginx/gzip.conf @@ -0,0 +1,29 @@ +gzip on; +gzip_disable "msie6"; + +gzip_vary on; +gzip_proxied any; +gzip_comp_level 6; +gzip_buffers 16 8k; +gzip_http_version 1.1; +gzip_min_length 256; +gzip_types + application/atom+xml + application/geo+json + application/javascript + application/x-javascript + application/json + application/ld+json + application/manifest+json + application/rdf+xml + application/rss+xml + application/xhtml+xml + application/xml + font/eot + font/otf + font/ttf + image/svg+xml + text/css + text/javascript + text/plain + text/xml; \ No newline at end of file diff --git a/nginx/nginx/hsts.conf b/nginx/nginx/hsts.conf new file mode 100644 index 0000000..a74dca5 --- /dev/null +++ b/nginx/nginx/hsts.conf @@ -0,0 +1 @@ +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; \ No newline at end of file diff --git a/nginx/nginx/nginx.sh b/nginx/nginx/nginx.sh new file mode 100644 index 0000000..49ea49e --- /dev/null +++ b/nginx/nginx/nginx.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -e + +if [ -z "$DOMAINS" ]; then + echo "DOMAINS environment variable is not set" + exit 1; +fi + +use_dummy_certificate() { + if grep -q "/etc/letsencrypt/live/$1" "/etc/nginx/sites/$1.conf"; then + echo "Switching Nginx to use dummy certificate for $1" + sed -i "s|/etc/letsencrypt/live/$1|/etc/nginx/sites/ssl/dummy/$1|g" "/etc/nginx/sites/$1.conf" + fi +} + +use_lets_encrypt_certificate() { + if grep -q "/etc/nginx/sites/ssl/dummy/$1" "/etc/nginx/sites/$1.conf"; then + echo "Switching Nginx to use Let's Encrypt certificate for $1" + sed -i "s|/etc/nginx/sites/ssl/dummy/$1|/etc/letsencrypt/live/$1|g" "/etc/nginx/sites/$1.conf" + fi +} + +reload_nginx() { + echo "Reloading Nginx configuration" + nginx -s reload +} + +wait_for_lets_encrypt() { + until [ -d "/etc/letsencrypt/live/$1" ]; do + echo "Waiting for Let's Encrypt certificates for $1" + sleep 5s & wait ${!} + done + use_lets_encrypt_certificate "$1" + reload_nginx +} + +if [ ! -f /etc/nginx/sites/ssl/ssl-dhparams.pem ]; then + mkdir -p "/etc/nginx/sites/ssl" + openssl dhparam -out /etc/nginx/sites/ssl/ssl-dhparams.pem 2048 +fi + +domains_fixed=$(echo "$DOMAINS" | tr -d \") +for domain in $domains_fixed; do + echo "Checking configuration for $domain" + + if [ ! -f "/etc/nginx/sites/$domain.conf" ]; then + echo "Creating Nginx configuration file /etc/nginx/sites/$domain.conf" + sed "s/\${domain}/$domain/g" /customization/site.conf.tpl > "/etc/nginx/sites/$domain.conf" + fi + + if [ ! -f "/etc/nginx/sites/ssl/dummy/$domain/fullchain.pem" ]; then + echo "Generating dummy ceritificate for $domain" + mkdir -p "/etc/nginx/sites/ssl/dummy/$domain" + printf "[dn]\nCN=${domain}\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:$domain, DNS:www.$domain\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth" > openssl.cnf + openssl req -x509 -out "/etc/nginx/sites/ssl/dummy/$domain/fullchain.pem" -keyout "/etc/nginx/sites/ssl/dummy/$domain/privkey.pem" \ + -newkey rsa:2048 -nodes -sha256 \ + -subj "/CN=${domain}" -extensions EXT -config openssl.cnf + rm -f openssl.cnf + fi + + if [ ! -d "/etc/letsencrypt/live/$domain" ]; then + use_dummy_certificate "$domain" + wait_for_lets_encrypt "$domain" & + else + use_lets_encrypt_certificate "$domain" + fi +done + +exec nginx -g "daemon off;" diff --git a/nginx/nginx/options-ssl-nginx.conf b/nginx/nginx/options-ssl-nginx.conf new file mode 100644 index 0000000..825b37c --- /dev/null +++ b/nginx/nginx/options-ssl-nginx.conf @@ -0,0 +1,8 @@ +ssl_session_cache shared:le_nginx_SSL:10m; +ssl_session_timeout 1440m; +ssl_session_tickets off; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers off; + +ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; diff --git a/nginx/nginx/site.conf.tpl b/nginx/nginx/site.conf.tpl new file mode 100644 index 0000000..c3491ef --- /dev/null +++ b/nginx/nginx/site.conf.tpl @@ -0,0 +1,28 @@ +server { + listen 80; + server_name ${domain} www.${domain}; + + location /.well-known/acme-challenge/ { + root /var/www/certbot/${domain}; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name ${domain} www.${domain}; + + ssl_certificate /etc/nginx/sites/ssl/dummy/${domain}/fullchain.pem; + ssl_certificate_key /etc/nginx/sites/ssl/dummy/${domain}/privkey.pem; + + include /etc/nginx/includes/options-ssl-nginx.conf; + + ssl_dhparam /etc/nginx/sites/ssl/ssl-dhparams.pem; + + include /etc/nginx/includes/hsts.conf; + + include /etc/nginx/vhosts/${domain}.conf; +} diff --git a/nginx/vhosts/felia.hwtr.dev.conf b/nginx/vhosts/felia.hwtr.dev.conf new file mode 100644 index 0000000..224b1bf --- /dev/null +++ b/nginx/vhosts/felia.hwtr.dev.conf @@ -0,0 +1,4 @@ +location / { + root /var/www/html/felia.hwtr.dev; + index index.html index.htm; +}