From cd24eacee41f037852ff5e48438fa8782b709237 Mon Sep 17 00:00:00 2001 From: moeny-matt Date: Thu, 17 Apr 2025 16:04:47 -0400 Subject: [PATCH] Add scripts to renew certs with certbot --- README.md | 11 ++--- check-and-renew-certs.sh | 49 +++++++++++++++++++++++ docker-compose.yml | 15 +++++++ renew-certs.sh | 86 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 check-and-renew-certs.sh create mode 100644 renew-certs.sh diff --git a/README.md b/README.md index 982acb7..cebf590 100644 --- a/README.md +++ b/README.md @@ -85,11 +85,11 @@ PORTAINER_PASSWORD= AI_OPENAI_API_KEY= ``` -3. Edit GoTrue Dockerfile +3. Edit GoTrue Dockerfile (Not needed on latest version) Modify the base Dockerfile from the Appflowy-Cloud repo to the one in this repo that installs bash, so the healthcheck can run. -4. Edit nginx.conf +4. Edit nginx.conf (Not needed on latest version) Modify the base nginx.conf to the version in this repo to resolve a potential websocket DNS resolution issue. @@ -140,13 +140,14 @@ docker-compose up -d 10. Set up auto renewal for the certificates with cron job: ```bash -sudo crontab -e +crontab -e ``` -Add this line to run the renewal daily (it will only renew if necessary): +Add this line to run the renewal every month on the 1st day at 2:00 AM: ```bash -0 3 * * * certbot renew --quiet --deploy-hook "docker-compose restart nginx" +0 2 1 * * /home/moeny/AppFlowy-Cloud/check-and-renew-certs.sh >> /var/log/cert-renewal.log 2>&1 ``` +You will need to add [check-and-renew-certs.sh](check-and-renew-certs.sh) and [renew-certs.sh](renew-certs.sh) in the `AppFlowy-Cloud` directory. ## Additional considerations diff --git a/check-and-renew-certs.sh b/check-and-renew-certs.sh new file mode 100644 index 0000000..d9e0f49 --- /dev/null +++ b/check-and-renew-certs.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Run this script with a cron job every month on the 1st day at 2:00 AM +# crontab -e +# 0 2 1 * * /home/moeny/AppFlowy-Cloud/check-and-renew-certs.sh >> /var/log/cert-renewal.log 2>&1 + +set -e + +# Configuration +COMPOSE_FILE="/home/moeny/AppFlowy-Cloud/docker-compose.yml" +DEPLOY_TRIGGER="/etc/letsencrypt/deploy-hook-triggered" + +# Ensure we're in the right directory +cd "$(dirname "$COMPOSE_FILE")" + +# Set environment variables to ensure non-interactive operation +export DEBIAN_FRONTEND=noninteractive +export PYTHONUNBUFFERED=1 + +# Run certbot container with explicit non-interactive settings and timeout +echo "[$(date)] Starting certificate renewal check..." +if timeout --signal=KILL 5m docker compose -f "$COMPOSE_FILE" run --rm \ + -e TERM=dumb \ + -e PYTHONUNBUFFERED=1 \ + --no-TTY \ + < /dev/null \ + certbot; then + echo "[$(date)] Certbot completed successfully" +else + exit_code=$? + if [ $exit_code -eq 137 ]; then # SIGKILL exit code + echo "[$(date)] ERROR: Certbot timed out after 5 minutes, killing container..." + # Find and kill any hanging certbot containers + docker ps --filter "name=certbot" -q | xargs -r docker kill + else + echo "[$(date)] ERROR: Certbot failed with exit code $exit_code" + fi + exit 1 +fi + +# Check if certificates were renewed +if [ -f "$DEPLOY_TRIGGER" ]; then + echo "[$(date)] Certificates were renewed, restarting nginx..." + docker compose -f "$COMPOSE_FILE" restart nginx + sudo rm "$DEPLOY_TRIGGER" + echo "[$(date)] Nginx restarted successfully" +else + echo "[$(date)] No certificate renewal needed" +fi \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9d7cdfc..04f2421 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -219,6 +219,21 @@ services: environment: - AF_BASE_URL=${APPFLOWY_BASE_URL:?err} - AF_GOTRUE_URL=${API_EXTERNAL_URL} + + certbot: + image: certbot/certbot + volumes: + - /etc/letsencrypt:/etc/letsencrypt + - /var/lib/letsencrypt:/var/lib/letsencrypt + - /home/moeny/AppFlowy-Cloud/renew-certs.sh:/renew-certs.sh + - /home/moeny/keys/tsig.key:/tsig.key:ro + environment: + - BIND_SERVER=ns1.moeny.ai + - DOMAIN=appflowy.moeny.ai + - EMAIL=appflowy-certbot@moeny.ai + - TSIG_KEY=/tsig.key + entrypoint: sh /renew-certs.sh + volumes: postgres_data: minio_data: \ No newline at end of file diff --git a/renew-certs.sh b/renew-certs.sh new file mode 100644 index 0000000..f6f9485 --- /dev/null +++ b/renew-certs.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +# Install required packages +apk add --no-cache bind-tools + +# Use environment variables with fallbacks +DOMAIN=${DOMAIN:-"appflowy.moeny.ai"} +EMAIL=${EMAIL:-"appflowy-certbot@moeny.ai"} +BIND_SERVER=${BIND_SERVER:-"ns1.moeny.ai"} +TSIG_KEY=${TSIG_KEY:-"/tsig.key"} # This will be the path inside the container +CERTBOT_DIR="/etc/letsencrypt" + +# Create the auth script with embedded functions +cat > /tmp/auth.sh << 'EOF' +#!/bin/sh + +# Function to add DNS challenge record +domain="$CERTBOT_DOMAIN" +token="$CERTBOT_VALIDATION" + +echo "Adding DNS challenge for $domain with token $token" +nsupdate -k "/tsig.key" << NSUPDATE +server $BIND_SERVER +update add _acme-challenge.$domain. 300 IN TXT "$token" +send +NSUPDATE + +# Wait for DNS propagation +echo "Waiting 30 seconds for DNS propagation..." +sleep 30 + +# Verify the record +echo "Verifying DNS record..." +if dig +short @$BIND_SERVER TXT _acme-challenge.$domain | grep -q "$token"; then + echo "DNS challenge record verified successfully" + exit 0 +else + echo "ERROR: DNS challenge record not found or incorrect" + exit 1 +fi +EOF +chmod +x /tmp/auth.sh + +# Create cleanup script with embedded function +cat > /tmp/cleanup.sh << 'EOF' +#!/bin/sh + +domain="$CERTBOT_DOMAIN" +echo "Removing DNS challenge for $domain" +nsupdate -k "/tsig.key" << NSUPDATE +server $BIND_SERVER +update delete _acme-challenge.$domain. TXT +send +NSUPDATE +EOF +chmod +x /tmp/cleanup.sh + +# Check if certificate already exists +if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then + echo "Certificate exists, attempting renewal..." + certbot renew \ + --manual \ + --preferred-challenges dns \ + --manual-auth-hook /tmp/auth.sh \ + --manual-cleanup-hook /tmp/cleanup.sh \ + --deploy-hook "touch /etc/letsencrypt/deploy-hook-triggered" \ + --force-renewal +else + echo "No certificate found, performing initial certificate request..." + certbot certonly \ + --manual \ + --preferred-challenges dns \ + --manual-auth-hook /tmp/auth.sh \ + --manual-cleanup-hook /tmp/cleanup.sh \ + --deploy-hook "touch /etc/letsencrypt/deploy-hook-triggered" \ + -d "$DOMAIN" \ + --email "$EMAIL" \ + --agree-tos \ + --non-interactive +fi + +# Clean up temporary scripts +rm -f /tmp/auth.sh /tmp/cleanup.sh + +echo "Certificate operation completed. Check /etc/letsencrypt/deploy-hook-triggered for successful deployment."