diff --git a/.env.example b/.env.example index 29c1af3..135f271 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ # required -DOMAIN= +DOMAIN= # comma-separated list for multiple domains API_KEY= SECRET_KEY= # optional API_URL=https://api.porkbun.com/api/json/v3 -CERTIFICATE_PATH=/etc/porkcron/certificate.pem -PRIVATE_KEY_PATH=/etc/porkcron/private_key.pem +CERTIFICATE_PATH=/etc/porkcron/{domain}/certificate.pem +PRIVATE_KEY_PATH=/etc/porkcron/{domain}/private_key.pem diff --git a/README.md b/README.md index 914f58f..83dc291 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,8 @@ Automatically renew SSL certificate for your Porkbun domain. ## 📌 About `porkcron` is a simple alternative to [certbot][1]. -If you own a domain registered by [Porkbun][2], -they offer you a [free SSL certificate][3] issued by [Let's Encrypt][4]. -So instead of getting it from scratch yourself, -you can periodically download the certificate using the [Porkbun API][5]. +If you own a domain registered by [Porkbun][2], they offer you a [free SSL certificate][3] issued by [Let's Encrypt][4]. +So instead of getting it from scratch yourself, you can periodically download the certificate using the [Porkbun API][5]. `porkcron` is designed to automate this process. It can be run as a [systemd timer][6] or in a Docker container. @@ -27,16 +25,24 @@ Take a look at the `.env.example` file. It contains all the environment variables used by `porkcron`. Rename it to `.env` and fill it with the values you got earlier. -| Name | Description | Required | Default | -|------------------|-------------------------------------|:--------:|-------------------------------------| -| DOMAIN | your Porkbun domain | yes | - | -| API_KEY | your Porkbun API key | yes | - | -| SECRET_KEY | your Porkbun API secret key | yes | - | -| API_URL | the Porkbun API address | no | https://api.porkbun.com/api/json/v3 | -| CERTIFICATE_PATH | the path to save the certificate to | no | /etc/porkcron/certificate.pem | -| PRIVATE_KEY_PATH | the path to save the private key to | no | /etc/porkcron/private_key.pem | +| Name | Description | Required | Default | +|------------------|-------------------------------------|:--------:|----------------------------------------| +| DOMAIN | your Porkbun domain(s) | yes | - | +| API_KEY | your Porkbun API key | yes | - | +| SECRET_KEY | your Porkbun API secret key | yes | - | +| API_URL | the Porkbun API address | no | https://api.porkbun.com/api/json/v3 | +| CERTIFICATE_PATH | the path to save the certificate to | no | /etc/porkcron/{domain}/certificate.pem | +| PRIVATE_KEY_PATH | the path to save the private key to | no | /etc/porkcron/{domain}/private_key.pem | -Now you need to choose the installation method. +Note the `{domain}` placeholder in the paths. +It will be automatically replaced with your domain. +You can use the placeholder in non-default paths as well. + +`porkcron` can also work with multiple domains at once. +You can set `DOMAIN` to a comma-separated list of domains. +In this case, both `CERTIFICATE_PATH` and `PRIVATE_KEY_PATH` must contain the `{domain}` placeholder. + +Once you have filled in all the values, you can proceed to choosing the installation method. ### Using systemd diff --git a/nginx/nginx.conf b/nginx/nginx.conf index ce54b1f..cf0358f 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -2,15 +2,15 @@ server { listen 443 ssl http2; listen [::]:443 ssl http2; - # server_name YOUR.DOMAIN; + # server_name your.domain; root /var/www/html; index index.html; location / { try_files $uri $uri/ =404; } - ssl_certificate /etc/porkcron/certificate.pem; - ssl_certificate_key /etc/porkcron/private_key.pem; + # ssl_certificate /path/to/certificate.pem; + # ssl_certificate_key /path/to/private_key.pem; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; diff --git a/nginx/wait-for-ssl.sh b/nginx/wait-for-ssl.sh index 0559aed..0c80603 100644 --- a/nginx/wait-for-ssl.sh +++ b/nginx/wait-for-ssl.sh @@ -2,6 +2,7 @@ ME=$(basename $0) +# TODO: support multiple domains while [ ! -e /etc/porkcron/certificate.pem ] || [ ! -e /etc/porkcron/private_key.pem ]; do echo "$ME: waiting for SSL bundle" sleep 1 diff --git a/porkcron.py b/porkcron.py index 73c2077..e3716e3 100644 --- a/porkcron.py +++ b/porkcron.py @@ -4,46 +4,62 @@ import logging import os import sys +from pathlib import Path from urllib import request # https://porkbun.com/api/json/v3/documentation DEFAULT_API_URL = "https://api.porkbun.com/api/json/v3" -DEFAULT_CERTIFICATE_PATH = "/etc/porkcron/certificate.crt" -DEFAULT_PRIVATE_KEY_PATH = "/etc/porkcron/private_key.key" +DEFAULT_CERTIFICATE_PATH = "/etc/porkcron/{domain}/certificate.pem" +DEFAULT_PRIVATE_KEY_PATH = "/etc/porkcron/{domain}/private_key.pem" + +DOMAIN_PLACEHOLDER = "{domain}" def main() -> None: logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logging.info("running SSL certificate renewal script") - domain = getenv_or_exit("DOMAIN") + domains = getenv_or_exit("DOMAIN").split(",") api_key = getenv_or_exit("API_KEY") secret_key = getenv_or_exit("SECRET_KEY") - url = os.getenv("API_URL", DEFAULT_API_URL) + "/ssl/retrieve/" + domain - body = json.dumps({"apikey": api_key, "secretapikey": secret_key}).encode() - headers = {"Content-Type": "application/json"} + certificate_path_template = os.getenv("CERTIFICATE_PATH", DEFAULT_CERTIFICATE_PATH) + if len(domains) > 1 and DOMAIN_PLACEHOLDER not in certificate_path_template: + exit(f"CERTIFICATE_PATH must contain the {DOMAIN_PLACEHOLDER} placeholder") + + private_key_path_template = os.getenv("PRIVATE_KEY_PATH", DEFAULT_PRIVATE_KEY_PATH) + if len(domains) > 1 and DOMAIN_PLACEHOLDER not in private_key_path_template: + exit(f"PRIVATE_KEY_PATH must contain the {DOMAIN_PLACEHOLDER} placeholder") + + for domain in domains: + url = os.getenv("API_URL", DEFAULT_API_URL) + "/ssl/retrieve/" + domain + body = json.dumps({"apikey": api_key, "secretapikey": secret_key}).encode() + headers = {"Content-Type": "application/json"} - logging.info(f"downloading SSL bundle for {domain}") - req = request.Request(url, data=body, headers=headers, method="POST") - with request.urlopen(req) as resp: - data = json.load(resp) + logging.info(f"downloading SSL bundle for {domain}") + req = request.Request(url, data=body, headers=headers, method="POST") + with request.urlopen(req) as resp: + data = json.load(resp) - if data["status"] == "ERROR": - logging.error(data["message"]) - sys.exit(1) + if data["status"] == "ERROR": + exit(data["message"]) - certificate_path = os.getenv("CERTIFICATE_PATH", DEFAULT_CERTIFICATE_PATH) - logging.info(f"saving certificate to {certificate_path}") - with open(certificate_path, "w") as f: - f.write(data["certificatechain"]) + certificate_path = Path(certificate_path_template.replace(DOMAIN_PLACEHOLDER, domain)) + logging.info(f"saving certificate to {certificate_path}") + certificate_path.parent.mkdir(parents=True, exist_ok=True) + certificate_path.write_text(data["certificatechain"]) - private_key_path = os.getenv("PRIVATE_KEY_PATH", DEFAULT_PRIVATE_KEY_PATH) - logging.info(f"saving private key to {private_key_path}") - with open(private_key_path, "w") as f: - f.write(data["privatekey"]) + private_key_path = Path(private_key_path_template.replace(DOMAIN_PLACEHOLDER, domain)) + logging.info(f"saving private key to {private_key_path}") + private_key_path.parent.mkdir(parents=True, exist_ok=True) + private_key_path.write_text(data["privatekey"]) - logging.info("SSL certificate has been successfully renewed") + logging.info(f"SSL certificate for {domain} has been renewed") + + +def exit(msg: str) -> None: + logging.error(msg) + sys.exit(1) def getenv_or_exit(key: str) -> str: