Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions ansible/playbooks/paas/roles/ansible-ufw/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
---
ufw_packages:
- ufw

ufw_ipv6: "yes"
ufw_default_input_policy: DROP
ufw_default_output_policy: ACCEPT
Expand Down Expand Up @@ -39,3 +36,7 @@ ufw_rules:
ufw_custom_rules: []

ufw_applications: []

# Blocklist configuration
ufw_blocklist_enabled: true
ufw_blocklist_url: "https://cdn.jsdelivr.net/gh/duggytuxy/Data-Shield_IPv4_Blocklist@refs/heads/main/prod_data-shield_ipv4_blocklist.txt"
4 changes: 4 additions & 0 deletions ansible/playbooks/paas/roles/ansible-ufw/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
- name: Reload ufw
community.general.ufw:
state: reloaded

- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
71 changes: 71 additions & 0 deletions ansible/playbooks/paas/roles/ansible-ufw/tasks/blocklist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
- name: Create blocklist working directory
ansible.builtin.file:
path: "{{ ufw_blocklist_tmp_dir }}"
state: directory
owner: root
group: root
mode: "0700"

- name: Deploy blocklist update script
ansible.builtin.template:
src: ufw-blocklist-update.sh.j2
dest: "{{ ufw_blocklist_script_path }}"
owner: root
group: root
mode: "0700"

- name: Create ipset for blocklist
ansible.builtin.shell: |
if ! ipset list {{ ufw_blocklist_ipset_name }} >/dev/null 2>&1; then
ipset create {{ ufw_blocklist_ipset_name }} hash:ip maxelem 200000
echo "CHANGED"
else
echo "OK"
fi
register: ipset_create
changed_when: "'CHANGED' in ipset_create.stdout"

- name: Ensure ipset match rule is in before.rules
ansible.builtin.lineinfile:
path: /etc/ufw/before.rules
insertafter: "^-A ufw-before-input -i lo -j ACCEPT"
line: "-A ufw-before-input -m set --match-set {{ ufw_blocklist_ipset_name }} src -j DROP"
state: present
notify: Reload ufw

- name: Deploy ipset restore systemd service
ansible.builtin.template:
src: ufw-blocklist-restore.service.j2
dest: /etc/systemd/system/ufw-blocklist-restore.service
owner: root
group: root
mode: "0644"
notify: Reload systemd

- name: Enable ipset restore service
ansible.builtin.systemd:
name: ufw-blocklist-restore.service
enabled: true
daemon_reload: true

- name: Reload ufw now if ipset rule was just added
ansible.builtin.meta: flush_handlers

- name: Run initial blocklist update
ansible.builtin.command: "{{ ufw_blocklist_script_path }}"
register: initial_update
changed_when: true
when: ipset_create is changed

- name: Configure cron job for blocklist updates
ansible.builtin.cron:
name: "{{ ufw_blocklist_ipset_name }}-update"
minute: "{{ ufw_blocklist_cron_minute }}"
hour: "{{ ufw_blocklist_cron_hour }}"
day: "{{ ufw_blocklist_cron_day }}"
month: "{{ ufw_blocklist_cron_month }}"
weekday: "{{ ufw_blocklist_cron_weekday }}"
job: "{{ ufw_blocklist_script_path }} 2>&1 | logger -t ufw-blocklist-cron"
user: root
state: present
4 changes: 4 additions & 0 deletions ansible/playbooks/paas/roles/ansible-ufw/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@
community.general.ufw:
state: "{{ ufw_state }}"
logging: "{{ ufw_logging }}"

- name: Configure blocklist
ansible.builtin.include_tasks: blocklist.yml
when: ufw_blocklist_enabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Restore UFW blocklist ipset before UFW starts
Before=ufw.service
After=network-pre.target

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'if [ -f {{ ufw_blocklist_ipset_save_file }} ]; then ipset restore < {{ ufw_blocklist_ipset_save_file }}; fi'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# Managed by Ansible - Do not edit manually
# Updates the {{ ufw_blocklist_ipset_name }} ipset from a remote IPv4 blocklist
set -euo pipefail

IPSET_NAME="{{ ufw_blocklist_ipset_name }}"
IPSET_TMP="${IPSET_NAME}-tmp"
BLOCKLIST_URL="{{ ufw_blocklist_url }}"
TMP_DIR="{{ ufw_blocklist_tmp_dir }}"
TMP_FILE="${TMP_DIR}/blocklist_new.txt"
CURRENT_FILE="${TMP_DIR}/blocklist_current.txt"
IPSET_SAVE_FILE="{{ ufw_blocklist_ipset_save_file }}"
LOG_TAG="ufw-blocklist"
LOCK_FILE="/var/run/ufw-blocklist-update.lock"
DRY_RUN=false
VERBOSE=false

for arg in "$@"; do
case "$arg" in
--dry-run|-n) DRY_RUN=true ;;
-v|--verbose) VERBOSE=true ;;
esac
done

log() {
if $DRY_RUN; then
echo "[DRY-RUN] $1"
elif $VERBOSE; then
echo "$1"
fi
logger -t "$LOG_TAG" "$1"
}

cleanup() {
rm -f "$LOCK_FILE" "$TMP_FILE" "${TMP_FILE}.clean"
# Destroy tmp set if it exists
ipset destroy "$IPSET_TMP" 2>/dev/null || true
}

trap cleanup EXIT

# Prevent concurrent runs
if [ -f "$LOCK_FILE" ]; then
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || true)
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
log "Another instance is already running (PID $LOCK_PID). Exiting."
exit 0
fi
log "Stale lock file found. Removing."
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"

# Ensure temp directory exists
mkdir -p "$TMP_DIR"

# Ensure the main ipset exists
if ! ipset list "$IPSET_NAME" >/dev/null 2>&1; then
if $DRY_RUN; then
log "Would create ipset: ipset create $IPSET_NAME hash:ip maxelem 200000"
else
log "ipset $IPSET_NAME does not exist. Creating."
ipset create "$IPSET_NAME" hash:ip maxelem 200000
fi
fi

# Download the blocklist
log "Downloading blocklist from $BLOCKLIST_URL"
if ! curl -fsSL --retry 3 --retry-delay 10 --max-time {{ ufw_blocklist_download_timeout }} \
-o "$TMP_FILE" "$BLOCKLIST_URL"; then
log "ERROR: Failed to download blocklist. Aborting update."
exit 1
fi

# Validate and clean the downloaded file: keep only valid IPv4 addresses
grep -Eo '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$' "$TMP_FILE" | sort -u > "${TMP_FILE}.clean"
mv "${TMP_FILE}.clean" "$TMP_FILE"

NEW_COUNT=$(wc -l < "$TMP_FILE")
if [ "$NEW_COUNT" -eq 0 ]; then
log "ERROR: Downloaded blocklist is empty. Aborting update."
exit 1
fi

# Sanity check: reject suspiciously small lists (possible corruption)
if [ -f "$CURRENT_FILE" ]; then
CURRENT_COUNT=$(wc -l < "$CURRENT_FILE")
if [ "$CURRENT_COUNT" -gt 0 ]; then
MIN_EXPECTED=$(( CURRENT_COUNT * {{ ufw_blocklist_min_ratio }} / 100 ))
if [ "$NEW_COUNT" -lt "$MIN_EXPECTED" ]; then
log "WARNING: New list ($NEW_COUNT IPs) is less than {{ ufw_blocklist_min_ratio }}% of current ($CURRENT_COUNT IPs). Aborting."
exit 1
fi
fi
fi

log "Downloaded $NEW_COUNT IPs."

if $DRY_RUN; then
# In dry-run, show what would change
if [ -f "$CURRENT_FILE" ]; then
ADD_COUNT=$(comm -23 "$TMP_FILE" "$CURRENT_FILE" | wc -l)
REMOVE_COUNT=$(comm -13 "$TMP_FILE" "$CURRENT_FILE" | wc -l)
else
ADD_COUNT="$NEW_COUNT"
REMOVE_COUNT=0
fi
log "Would add: $ADD_COUNT IPs, Would remove: $REMOVE_COUNT IPs, Total: $NEW_COUNT IPs"
log "Would swap ipset $IPSET_TMP -> $IPSET_NAME"
log "Would save ipset to $IPSET_SAVE_FILE"
exit 0
fi

# Create temporary ipset and populate it
ipset create "$IPSET_TMP" hash:ip maxelem 200000

log "Populating temporary ipset $IPSET_TMP with $NEW_COUNT IPs..."
while IFS= read -r ip; do
[ -z "$ip" ] && continue
ipset add "$IPSET_TMP" "$ip" 2>/dev/null || true
done < "$TMP_FILE"

# Atomic swap
ipset swap "$IPSET_TMP" "$IPSET_NAME"
log "Swapped $IPSET_TMP -> $IPSET_NAME"

# Destroy the old set (now under the tmp name)
ipset destroy "$IPSET_TMP" 2>/dev/null || true

# Save for persistence across reboots
ipset save "$IPSET_NAME" > "$IPSET_SAVE_FILE"
log "Saved ipset to $IPSET_SAVE_FILE"

# Save the current list for next run's sanity check
cp "$TMP_FILE" "$CURRENT_FILE"

TOTAL=$(ipset list "$IPSET_NAME" 2>/dev/null | grep -c "^[0-9]" || true)
log "Update complete. Total blocked: $TOTAL IPs"
25 changes: 25 additions & 0 deletions ansible/playbooks/paas/roles/ansible-ufw/vars/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
ufw_packages:
- ufw
- ipset

ufw_blocklist_ipset_name: ufw-blocklist-user

ufw_blocklist_script_path: /usr/local/sbin/ufw-blocklist-update.sh

ufw_blocklist_tmp_dir: /var/lib/ufw-blocklist

ufw_blocklist_ipset_save_file: /var/lib/ufw-blocklist/ipset.save

ufw_blocklist_download_timeout: 60

# Minimum ratio (%) of new list size vs current list to accept the update
# Protects against corrupted/empty downloads replacing a valid blocklist
ufw_blocklist_min_ratio: 50

# Cron schedule - default every 6 hours matching upstream refresh rate
ufw_blocklist_cron_minute: "15"
ufw_blocklist_cron_hour: "*/6"
ufw_blocklist_cron_day: "*"
ufw_blocklist_cron_month: "*"
ufw_blocklist_cron_weekday: "*"
Loading