8.6 KiB
CDN Update Script
This document describes a reference implementation of the cdn-update.sh automation script for the Community CDN architecture.
The script is designed to:
- Download the signed control file
- Verify its signature
- Validate JSON syntax
- Generate fail2ban configuration
- Generate nginx configuration
- Select an available origin
- Synchronize content using rsync
- Export Prometheus metrics
- Fail safely when configuration validation fails
Reference Script
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# Configuration
###############################################################################
BASE="/var/lib/cdn"
CONFIG_DIR="${BASE}/config"
CONTENT_DIR="${BASE}/content"
METRICS_DIR="${BASE}/metrics"
CONTROL_URL="https://control.example.org/hpr.ccdn.settings.json"
SIG_URL="https://control.example.org/hpr.ccdn.settings.json.minisig"
PUBKEY="RWQxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
CONTROL_FILE="${CONFIG_DIR}/hpr.ccdn.settings.json"
SIG_FILE="${CONFIG_DIR}/hpr.ccdn.settings.json.minisig"
NGINX_GEN="/etc/nginx/conf.d/cdn-generated.conf"
FAIL2BAN_JAIL="/etc/fail2ban/jail.d/cdn-generated.local"
METRICS_FILE="${METRICS_DIR}/cdn.prom"
TMPDIR="$(mktemp -d)"
###############################################################################
# Metrics helper
###############################################################################
metric_write() {
cat > "${METRICS_FILE}" <<EOF
cdn_last_sync_timestamp ${LAST_SYNC_TIMESTAMP:-0}
cdn_sync_success ${SYNC_SUCCESS:-0}
cdn_sync_duration_seconds ${SYNC_DURATION:-0}
cdn_invalid_requests_total ${INVALID_REQUESTS:-0}
cdn_active_origin{origin="${ACTIVE_ORIGIN:-none}"} 1
EOF
}
###############################################################################
# Cleanup
###############################################################################
cleanup() {
rm -rf "${TMPDIR}"
}
trap cleanup EXIT
###############################################################################
# Download control file
###############################################################################
echo "Downloading control file..."
curl -fsSL \
-o "${TMPDIR}/hpr.ccdn.settings.json" \
"${CONTROL_URL}"
curl -fsSL \
-o "${TMPDIR}/hpr.ccdn.settings.json.minisig" \
"${SIG_URL}"
###############################################################################
# Verify signature
###############################################################################
echo "Verifying signature..."
minisign \
-Vm "${TMPDIR}/hpr.ccdn.settings.json" \
-P "${PUBKEY}" \
-x "${TMPDIR}/hpr.ccdn.settings.json.minisig"
###############################################################################
# Validate JSON
###############################################################################
jq empty "${TMPDIR}/hpr.ccdn.settings.json"
install -m 0644 \
"${TMPDIR}/hpr.ccdn.settings.json" \
"${CONTROL_FILE}"
install -m 0644 \
"${TMPDIR}/hpr.ccdn.settings.json.minisig" \
"${SIG_FILE}"
###############################################################################
# Load values
###############################################################################
mapfile -t ORIGINS < <(
jq -r '.origins[]' "${CONTROL_FILE}"
)
RSYNC_INTERVAL=$(
jq -r '.rsync_interval_hours' "${CONTROL_FILE}"
)
MAXRETRY=$(
jq -r '.fail2ban.maxretry' "${CONTROL_FILE}"
)
FINDTIME=$(
jq -r '.fail2ban.findtime' "${CONTROL_FILE}"
)
BANTIME=$(
jq -r '.fail2ban.bantime' "${CONTROL_FILE}"
)
###############################################################################
# Generate fail2ban configuration
###############################################################################
echo "Generating fail2ban configuration..."
ADMIN_IPS=$(
jq -r '.admin_ips[]?' "${CONTROL_FILE}" \
| tr '\n' ' '
)
cat > "${FAIL2BAN_JAIL}" <<EOF
[nginx-invalid]
enabled = true
maxretry = ${MAXRETRY}
findtime = ${FINDTIME}
bantime = ${BANTIME}
ignoreip = 127.0.0.1 ${ADMIN_IPS}
EOF
systemctl reload fail2ban
###############################################################################
# Select active origin
###############################################################################
ACTIVE_ORIGIN=""
for ORIGIN in "${ORIGINS[@]}"
do
if ssh \
-o BatchMode=yes \
-o ConnectTimeout=5 \
"${ORIGIN}" \
true
then
ACTIVE_ORIGIN="${ORIGIN}"
break
fi
done
if [ -z "${ACTIVE_ORIGIN}" ]
then
echo "No origin available"
exit 1
fi
###############################################################################
# Generate nginx configuration
###############################################################################
echo "Generating nginx config..."
EXT_REGEX=$(
jq -r '.allowed_extensions[]' "${CONTROL_FILE}" \
| paste -sd'|' -
)
cat > "${NGINX_GEN}" <<EOF
autoindex off;
location ~ ^/eps/hpr[0-9]{4}/hpr[0-9]{4}\.(${EXT_REGEX})\$ {
root ${CONTENT_DIR}/public_html;
}
location = /robots.txt {
root ${CONTENT_DIR}/public_html;
}
location = /favicon.ico {
root ${CONTENT_DIR}/public_html;
}
location / {
access_log /var/log/nginx/invalid_requests.log;
return 404;
}
EOF
nginx -t
systemctl reload nginx
###############################################################################
# Rsync
###############################################################################
echo "Starting rsync..."
START_TIME=$(date +%s)
if rsync \
-az \
--delete-delay \
rsyncuser@"${ACTIVE_ORIGIN}":/srv/content/ \
"${CONTENT_DIR}/"
then
END_TIME=$(date +%s)
LAST_SYNC_TIMESTAMP="${END_TIME}"
SYNC_DURATION=$((END_TIME - START_TIME))
SYNC_SUCCESS=1
else
LAST_SYNC_TIMESTAMP=$(date +%s)
SYNC_DURATION=0
SYNC_SUCCESS=0
fi
###############################################################################
# Invalid request metric
###############################################################################
INVALID_REQUESTS=$(
wc -l \
< /var/log/nginx/invalid_requests.log \
|| echo 0
)
###############################################################################
# Write metrics
###############################################################################
metric_write
echo "Update complete"
Recommended Enhancements
Atomic Configuration Updates
Generate temporary configuration files first and only replace active files after validation succeeds.
Example workflow:
- Generate configuration in a temporary directory.
- Run
nginx -t. - Replace production configuration.
- Reload nginx.
This prevents broken configuration from affecting service availability.
SSH Host Key Pinning
Use a dedicated known_hosts file.
Example:
ssh \
-o UserKnownHostsFile=/etc/cdn/known_hosts \
-o StrictHostKeyChecking=yes
This protects against origin impersonation.
Restrict Synchronized Content
Limit rsync to approved file types.
Example:
rsync \
-az \
--delete-delay \
--include='*/' \
--include='*.mp3' \
--include='*.ogg' \
--include='*.opus' \
--include='*.txt' \
--include='*.json' \
--exclude='*' \
rsyncuser@origin:/srv/content/ \
/var/lib/cdn/content/
This prevents accidental synchronization of unexpected files.
Cached Control Files
If the control server is temporarily unavailable:
- Continue serving content
- Continue using the last verified control file
- Retry on the next scheduled execution
Nodes should never accept an unsigned replacement file.
Version-Based Synchronization
Store the last control file version.
Only perform a full synchronization when:
- The control file version changes
force_full_rsyncis enabled
This reduces unnecessary origin traffic.
Fail2ban Dynamic Ban Includes
Generate a separate include file for:
- Immediate IP bans
- Dynamic blocklists
Avoid rewriting the primary jail configuration on every update.
Example Cron Schedule
*/5 * * * * /usr/local/bin/cdn-update.sh
This provides:
- Control file refresh every 5 minutes
- Automatic failover detection
- Automatic configuration updates
- Regular synchronization scheduling
Operational Flow
- Download control file.
- Verify minisign signature.
- Validate JSON.
- Generate fail2ban configuration.
- Generate nginx configuration.
- Select active origin.
- Synchronize content.
- Export Prometheus metrics.
- Exit successfully.
If any validation step fails, the script exits without modifying the running configuration.
This fail-closed behavior helps ensure that only authenticated, valid configuration changes are applied to CDN nodes.