Skip to content

Commit

Permalink
feat: support multiple domains (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmzane authored Feb 11, 2025
1 parent 74999f6 commit 15e043c
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 41 deletions.
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
32 changes: 19 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions nginx/wait-for-ssl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 38 additions & 22 deletions porkcron.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 15e043c

Please sign in to comment.