Skip to content

Commit ac7270b

Browse files
Merge pull request #18 from 8bitDream/copilot/add-certbot-certificate-support
Add EC2 certbot automation for Let’s Encrypt issuance and renewal
2 parents be6f852 + e9f3dc2 commit ac7270b

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,7 @@ images/coming soon
9494

9595
Pipfile
9696
Pipfile.*
97+
98+
# Runtime TLS certificates copied from /etc/letsencrypt/live
99+
fullchain.pem
100+
privkey.pem

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,36 @@ More APIs examples can be found here: [https://www.amiiboapi.org/docs/](https://
4040
2. Install the requirements using `pip install -r requirements.txt`
4141
3. Run `app.py` or launch via `gunicorn app:app`
4242

43+
### SSL Certificate Setup (EC2 + Let's Encrypt / Certbot)
44+
For EC2 deployments where the app runs directly from this project root (no `/www` directory), use:
45+
46+
```bash
47+
chmod +x scripts/certbot_certificate.sh
48+
./scripts/certbot_certificate.sh all
49+
```
50+
51+
This script:
52+
- Registers/requests a certificate with certbot (`amiiboapi.org,www.amiiboapi.org` by default)
53+
- Copies `fullchain.pem` and `privkey.pem` from `/etc/letsencrypt/live/amiiboapi.org/` into the project root
54+
- Sets file permissions to read/write for owner+group (`660`) on both certificate files
55+
- Installs `/etc/cron.d/amiiboapi-certbot` to run renewal checks twice daily
56+
- `certbot renew` attempts renewal for certificates with 30 days or less remaining (90-day validity period)
57+
58+
> [!IMPORTANT]
59+
> `certbot --standalone` needs port `80` available. Stop any process using port `80` before running issuance if needed.
60+
61+
If needed, set custom domains:
62+
63+
```bash
64+
CERTBOT_DOMAINS="example.org,www.example.org" CERTBOT_PRIMARY_DOMAIN="example.org" ./scripts/certbot_certificate.sh all
65+
```
66+
67+
Optional (recommended) email for Let's Encrypt expiration notices:
68+
69+
```bash
70+
CERTBOT_EMAIL="admin@example.org" ./scripts/certbot_certificate.sh all
71+
```
72+
4373
### Heroku Setup (if you want to host)
4474
Click on the `Deploy to Heroku` button and you are good to go!
4575
*Heroku is a paid service and requires an account to use*

scripts/certbot_certificate.sh

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
SCRIPT_PATH="$PROJECT_ROOT/scripts/certbot_certificate.sh"
6+
7+
DOMAINS="${CERTBOT_DOMAINS:-amiiboapi.org,www.amiiboapi.org}"
8+
PRIMARY_DOMAIN="${CERTBOT_PRIMARY_DOMAIN:-amiiboapi.org}"
9+
CERTBOT_EMAIL="${CERTBOT_EMAIL:-}"
10+
CERTBOT_LIVE_DIR="/etc/letsencrypt/live/$PRIMARY_DOMAIN"
11+
12+
DEST_FULLCHAIN="$PROJECT_ROOT/fullchain.pem"
13+
DEST_PRIVKEY="$PROJECT_ROOT/privkey.pem"
14+
15+
CRON_FILE="/etc/cron.d/amiiboapi-certbot"
16+
LOG_FILE="/var/log/amiiboapi-certbot.log"
17+
18+
run_as_root() {
19+
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
20+
"$@"
21+
else
22+
sudo "$@"
23+
fi
24+
}
25+
26+
sync_certificates_to_project_root() {
27+
local owner_group
28+
29+
run_as_root test -f "$CERTBOT_LIVE_DIR/fullchain.pem"
30+
run_as_root test -f "$CERTBOT_LIVE_DIR/privkey.pem"
31+
32+
if run_as_root test -f "$DEST_FULLCHAIN"; then
33+
owner_group="$(run_as_root stat -c '%U:%G' "$DEST_FULLCHAIN")"
34+
elif [ -n "${SUDO_USER:-}" ]; then
35+
owner_group="${SUDO_USER}:${SUDO_USER}"
36+
else
37+
owner_group="$(id -un):$(id -gn)"
38+
fi
39+
40+
run_as_root cp "$CERTBOT_LIVE_DIR/fullchain.pem" "$DEST_FULLCHAIN"
41+
run_as_root cp "$CERTBOT_LIVE_DIR/privkey.pem" "$DEST_PRIVKEY"
42+
43+
run_as_root chown "$owner_group" "$DEST_FULLCHAIN" "$DEST_PRIVKEY"
44+
run_as_root chmod 660 "$DEST_FULLCHAIN" "$DEST_PRIVKEY"
45+
}
46+
47+
issue_certificate() {
48+
local certbot_args
49+
50+
certbot_args=(certonly --standalone --non-interactive --agree-tos --domains "$DOMAINS")
51+
if [ -n "$CERTBOT_EMAIL" ]; then
52+
certbot_args+=(--email "$CERTBOT_EMAIL")
53+
else
54+
certbot_args+=(--register-unsafely-without-email)
55+
fi
56+
57+
# --standalone uses an internal web server and requires port 80 to be available.
58+
if ! run_as_root certbot "${certbot_args[@]}"; then
59+
echo "Certificate issuance failed. Ensure DNS points to this host and port 80 is available for certbot --standalone." >&2
60+
exit 1
61+
fi
62+
sync_certificates_to_project_root
63+
}
64+
65+
renew_certificate() {
66+
if ! run_as_root certbot renew; then
67+
echo "Certificate renewal failed. Check certbot output above and /var/log/amiiboapi-certbot.log for details." >&2
68+
exit 1
69+
fi
70+
sync_certificates_to_project_root
71+
}
72+
73+
install_renewal_schedule() {
74+
local quoted_script_path quoted_log_file cron_cmd cron_line
75+
if [[ "$SCRIPT_PATH" != /* || "$LOG_FILE" != /* || "$SCRIPT_PATH" == *$'\n'* || "$LOG_FILE" == *$'\n'* ]]; then
76+
echo "SCRIPT_PATH and LOG_FILE must be absolute single-line paths for cron setup." >&2
77+
exit 1
78+
fi
79+
80+
printf -v quoted_script_path '%q' "$SCRIPT_PATH"
81+
printf -v quoted_log_file '%q' "$LOG_FILE"
82+
cron_cmd="/bin/bash $quoted_script_path renew >> $quoted_log_file 2>&1"
83+
cron_line="0 3,15 * * * root $cron_cmd"
84+
85+
run_as_root touch "$LOG_FILE"
86+
run_as_root chmod 644 "$LOG_FILE"
87+
printf '%s\n' "$cron_line" | run_as_root tee "$CRON_FILE" >/dev/null
88+
run_as_root chmod 644 "$CRON_FILE"
89+
}
90+
91+
usage() {
92+
cat <<EOF
93+
Usage: $(basename "$0") [issue|renew|install-cron|all]
94+
95+
Commands:
96+
issue Request/refresh the certificate for domains: $DOMAINS
97+
renew Run certbot renew and re-copy certificates to project root
98+
install-cron Install /etc/cron.d schedule for daily renewal checks
99+
all issue + install-cron (default)
100+
101+
Environment variables:
102+
CERTBOT_DOMAINS Comma-separated domain list (default: $DOMAINS)
103+
CERTBOT_PRIMARY_DOMAIN Domain used under /etc/letsencrypt/live (default: $PRIMARY_DOMAIN)
104+
CERTBOT_EMAIL Contact email for Let's Encrypt registration
105+
EOF
106+
}
107+
108+
main() {
109+
local command="${1:-all}"
110+
111+
case "$command" in
112+
issue)
113+
issue_certificate
114+
;;
115+
renew)
116+
renew_certificate
117+
;;
118+
install-cron)
119+
install_renewal_schedule
120+
;;
121+
all)
122+
issue_certificate
123+
install_renewal_schedule
124+
;;
125+
-h|--help|help)
126+
usage
127+
;;
128+
*)
129+
echo "Unknown command: $command" >&2
130+
usage
131+
exit 1
132+
;;
133+
esac
134+
}
135+
136+
main "$@"

0 commit comments

Comments
 (0)