2026-08-07_13-20-43_CEST
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
#!/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:
|
||||
|
||||
1. Generate configuration in a temporary directory.
|
||||
2. Run `nginx -t`.
|
||||
3. Replace production configuration.
|
||||
4. Reload nginx.
|
||||
|
||||
This prevents broken configuration from affecting service availability.
|
||||
|
||||
---
|
||||
|
||||
## SSH Host Key Pinning
|
||||
|
||||
Use a dedicated `known_hosts` file.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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_rsync` is 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
|
||||
|
||||
```cron
|
||||
*/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
|
||||
|
||||
1. Download control file.
|
||||
2. Verify minisign signature.
|
||||
3. Validate JSON.
|
||||
4. Generate fail2ban configuration.
|
||||
5. Generate nginx configuration.
|
||||
6. Select active origin.
|
||||
7. Synchronize content.
|
||||
8. Export Prometheus metrics.
|
||||
9. 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.
|
||||
Reference in New Issue
Block a user