diff --git a/.gitignore b/.gitignore index 8606e39..4dc62f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /nginx/domains.txt -/nginx/servers.json /nginx/servers.yaml +/nginx/nginx.conf.j2 +/nginx.conf *.env +/.venv +/docker-compose.y*ml \ No newline at end of file diff --git a/README.md b/README.md index 517a03b..44d53fb 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,17 @@ ## Preparation -Add your domains for _certbot_ to [domains.txt file at nginx directory](nginx/domains.txt) and servers configuration - json file in format -```json -{ - "domain": { - "redirect": "redirection_path", - "options": [ - "some_nginx_option_on_server_level here;" - ] - } -} -``` -to [servers.json file at nginx directory](nginx/servers.json). +1. Copy and edit (if needed) [nginx/nginx.conf.j2](nginx/nginx.conf.j2.example) file. +2. Add your certified domains to nginx/domains.txt file ([example](nginx/domains.txt.example)). + + These domains will be used by _certbot_ to monitor and update (if possible) certificates. _Nginx_ will also setup + http server for the given entries. + + However if the domain is set both in domains.txt and servers.yaml (next step), _nginx_ will + use https with certificates at the given path. That will fail nginx startup if the certificate + to at least one domain is missing (--http-only option will skip domains.txt check). + +3. Add your servers section configuration to nginx/servers.yaml ([example](nginx/servers.yaml.example)). User email used for certbot can be set as environment variable at build process or in .env file. diff --git a/docker-compose.yml b/docker-compose.yaml.example similarity index 82% rename from docker-compose.yml rename to docker-compose.yaml.example index 205713f..9e63a79 100644 --- a/docker-compose.yml +++ b/docker-compose.yaml.example @@ -23,16 +23,10 @@ services: ports: - 80:80 - 443:443 - networks: - - hosting_net restart: unless-stopped volumes: ssl: name: ssl_nginx_ssl letsencrypt: - name: ssl_nginx_letsencrypt - -networks: - hosting_net: - external: true + name: ssl_nginx_letsencrypt diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 03a2fd3..cbda994 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-alpine as builder +FROM python:3.11-alpine AS builder RUN pip3 install --no-cache-dir pyyaml jinja2 @@ -16,12 +16,11 @@ FROM nginx:alpine COPY --from=builder /nginx.conf /etc/nginx/nginx.conf COPY domains.txt /domains.txt -RUN echo "16 2 */7 * * nginx -s reload" > /etc/crontabs/root && \ +RUN echo "20 2 */7 * * nginx -s reload" > /etc/crontabs/root && \ \ echo "cp /domains.txt /ssl/domains.txt" > /entrypoint && \ echo "crond" >> /entrypoint && \ - echo "nginx -g 'daemon off;'" >> /entrypoint && \ - echo "nginx" >> /entrypoint + echo "nginx -g 'daemon off;'" >> /entrypoint ENTRYPOINT ["/bin/sh"] CMD ["/entrypoint"] diff --git a/nginx/add_servers.py b/nginx/add_servers.py index 4f3f98c..0a19afe 100644 --- a/nginx/add_servers.py +++ b/nginx/add_servers.py @@ -1,4 +1,5 @@ """Add domain servers to nginx.conf executable script.""" + from __future__ import annotations import argparse @@ -16,7 +17,7 @@ class Server: name: str all_names: str | None = None - redirect: str | None = None + proxy_pass: str | None = None certificate_dir: str | None = None port: int = None ssl_port: int = None @@ -42,7 +43,7 @@ class Server: return { "name": self.name, "all_names": self.all_names, - "redirect": self.redirect, + "proxy_pass": self.proxy_pass, "server_options": self.server_options, "location_options": self.location_options, "certificate_dir": self.certificate_dir, @@ -105,19 +106,25 @@ def main() -> None: domains_with_certs = [] nginx_servers: list[Server] = [] + resolver: str = "127.0.0.1" + acme_challenge_location: str | None = None + if args.servers_config is not None: with open(args.servers_config, "r", encoding="utf-8") as file: data: dict = yaml.safe_load(file) - resolver: str = data.get("resolver", "127.0.0.1") - acme_challenge_location: str | None = data.get("acme_challenge_location") + resolver: str = data.get("resolver", resolver) + acme_challenge_location = data.get("acme_challenge_location") servers: dict[str, dict[str, Any]] = data["servers"] for server_name, params in servers.items(): nginx_servers.append( Server( - name=(server_name if "*" not in server_name else f"{server_name} {server_name.replace('*.', '')}"), - all_names=params.get("all_names"), - redirect=params.get("redirect"), + name=server_name, + all_names=params.get( + "all_names", + None if "*" not in server_name else f"{server_name} {server_name.replace('*.', '', 1)}", + ), + proxy_pass=params.get("proxy_pass"), certificate_dir=_get_certificate_path( args.http_only, domains_with_certs, args.certificates_path, server_name ), @@ -130,8 +137,12 @@ def main() -> None: for domain in domains_with_certs: if not any( ( - domain == server.name - or (domain == f"*.{server.name[server.name.find('.') + 1:]}" if "." in server.name else False) + (server.all_names is None and domain == server.name) + or ( + server.all_names is not None + and f" {domain}" in server.all_names + or server.all_names.startswith(domain) + ) ) for server in nginx_servers ): diff --git a/nginx/domains.txt.example b/nginx/domains.txt.example index e3488c9..cac09cc 100644 --- a/nginx/domains.txt.example +++ b/nginx/domains.txt.example @@ -1,5 +1,5 @@ -your.domain.to_redirect +your.domain.to_listen your_other.doma.in *.doma.in #commented.domain -domain.without.redirect +domain.without.proxy diff --git a/nginx/get-certificates.Dockerfile b/nginx/get-certificates.Dockerfile index 00eb5cb..1d07a5e 100644 --- a/nginx/get-certificates.Dockerfile +++ b/nginx/get-certificates.Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-alpine as builder +FROM python:3.10-alpine AS builder COPY add_servers.py /add_servers.py COPY domains.txt /domains.txt @@ -18,8 +18,7 @@ COPY domains.txt /domains.txt RUN echo "(sleep 120 && killall nginx) &" > /entrypoint && \ echo "cp /domains.txt /ssl/domains.txt" >> /entrypoint && \ - echo "nginx -g 'daemon off;'" >> /entrypoint && \ - echo "nginx" >> /entrypoint + echo "nginx -g 'daemon off;'" >> /entrypoint ENTRYPOINT ["/bin/sh"] CMD ["/entrypoint"] diff --git a/nginx/nginx.conf.j2 b/nginx/nginx.conf.j2.example similarity index 80% rename from nginx/nginx.conf.j2 rename to nginx/nginx.conf.j2.example index 6d191eb..17f2ea1 100644 --- a/nginx/nginx.conf.j2 +++ b/nginx/nginx.conf.j2.example @@ -1,82 +1,83 @@ -{#- variables ~ examples: #} -{#- acme_challenge_location ~ /ssl/: #} -{#- resolver ~ 127.0.0.11: #} -{#- servers ~ ["name": ..., ("redirect": ..., "server_options": ..., "location_options": ..., "all_names": ..., "port": ..., "ssl_port": ...)]: #} -{#- #}user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log notice; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; -} - -http { - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - server_tokens off; - gzip on; - - proxy_connect_timeout 300; - proxy_send_timeout 600; - proxy_read_timeout 600; - send_timeout 600; - client_max_body_size 500M; - - server { - return 404; - } - - {%- for server in servers %} - server { - listen {{ server["port"] or 80 }}; - {%- if server["certificate_dir"] is not none %} - listen {{ server["ssl_port"] or 443 }} ssl; - {%- endif %} - keepalive_timeout 70; - - server_name {{ server["all_names"] or server["name"] }}; - - {%- if acme_challenge_location is defined %} - {# #} - location /.well-known/acme-challenge { - root {{ acme_challenge_location }}; - } - {%- endif %} - - {%- if server["server_options"]|length > 0 %} - {# #} - {%- for server_option in server["server_options"] %} - {{ server_option }} - {%- endfor %} - {%- endif %} - - {%- if server["certificate_dir"] is not none %} - {# #} - ssl_certificate {{ server["certificate_dir"] }}/fullchain.pem; - ssl_certificate_key {{ server["certificate_dir"] }}/privkey.pem; - {%- endif %} - - {%- if server["redirect"] is not none %} - {# #} - location / { - resolver {{ resolver }}; - set $host_{{ loop.index }} {{ server["redirect"] }}; - proxy_pass $host_{{ loop.index }}; - - proxy_set_header Host $host:$server_port; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - - {%- if server["location_options"]|length > 0 %} - {# #} - {%- for location_option in server["location_options"] %} - {{ location_option }} - {%- endfor %} - {%- endif %} - } - {%- endif %} - } - {%- endfor %} -} +{#- variables ~ examples: #} +{#- acme_challenge_location ~ /ssl/: #} +{#- resolver ~ 127.0.0.11: #} +{#- servers ~ ["name": ..., ("proxy_pass": ..., "server_options": ..., "location_options": ..., "all_names": ..., "port": ..., "ssl_port": ...)]: #} +{#- #}user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + server_tokens off; + gzip on; + + proxy_connect_timeout 300; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; + client_max_body_size 500M; + + server { + return 404; + } + + {%- for server in servers %} + {# #} + server { + listen {{ server["port"] or 80 }}; + {%- if server["certificate_dir"] is not none %} + listen {{ server["ssl_port"] or 443 }} ssl; + ssl_certificate {{ server["certificate_dir"] }}/fullchain.pem; + ssl_certificate_key {{ server["certificate_dir"] }}/privkey.pem; + + if ($scheme = 'http') { + return 302 https://$host$request_uri; + } + {%- endif %} + keepalive_timeout 70; + + server_name {{ server["all_names"] or server["name"] }}; + + {%- if acme_challenge_location is not none %} + {# #} + location /.well-known/acme-challenge { + root {{ acme_challenge_location }}; + } + {%- endif %} + + {%- if server["server_options"]|length > 0 %} + {# #} + {%- for server_option in server["server_options"] %} + {{ server_option }} + {%- endfor %} + {%- endif %} + + {%- if server["proxy_pass"] is not none %} + {# #} + location / { + resolver {{ resolver }}; + set $host_{{ loop.index }} {{ server["proxy_pass"] }}; + proxy_pass $host_{{ loop.index }}; + + proxy_set_header Host $host:$server_port; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + + {%- if server["location_options"]|length > 0 %} + {# #} + {%- for location_option in server["location_options"] %} + {{ location_option }} + {%- endfor %} + {%- endif %} + } + {%- endif %} + } + {%- endfor %} +} diff --git a/nginx/servers.yaml.example b/nginx/servers.yaml.example index 2db8554..f846659 100644 --- a/nginx/servers.yaml.example +++ b/nginx/servers.yaml.example @@ -1,11 +1,12 @@ resolver: 127.0.0.1 acme_challenge_location: /etc/nginx/acme/ + servers: your.domain.to_redirect: - redirect: "http://redirection.address" + proxy_pass: "http://redirection.address" your_other.doma.in: - redirect: "http://redirection-other.address" + proxy_pass: "http://redirection-other.address" server_options: - "proxy_buffering off;" - "proxy_request_buffering off;" @@ -16,7 +17,7 @@ servers: - "proxy_read_timeout 86400;" "*.doma.in": all_names: "*.doma.in doma.in" - redirect: "http://full.subdomain.redirect" + proxy_pass: "http://full.subdomain.proxy" server_options: - "proxy_buffering off;" - "proxy_request_buffering off;" diff --git a/pyproject.toml b/pyproject.toml index 140c9fc..38e77ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ +# dummy pyproject file for linting [tool.pylint] max-line-length = 120 [tool.black] -line-length = 120 \ No newline at end of file +line-length = 120