Harden quarantine provisioning; enforce strict permissions and update Ansible and docs

This commit is contained in:
2026-02-12 07:47:48 +01:00
parent 037b176892
commit 1768f61da1
44 changed files with 2587 additions and 698 deletions

View File

@@ -0,0 +1,104 @@
---
# Full Ansible playbook to provision upload-logger directories, permissions, tmpfiles and logrotate.
# Usage: ansible-playbook -i inventory scripts/ansible/provision-full.yml
- hosts: web
become: true
vars:
upload_logger_root: "{{ playbook_dir | default('.') | dirname | realpath }}"
logs_dir: "{{ upload_logger_root }}/logs"
quarantine_dir: "{{ upload_logger_root }}/quarantine"
state_dir: "{{ upload_logger_root }}/state"
examples_dir: "{{ upload_logger_root }}/examples"
quarantine_owner: "root"
quarantine_group: "www-data"
quarantine_perms: "0700"
state_perms: "0750"
logs_perms: "0750"
log_file_mode: "0640"
selinux_fcontext: "httpd_sys_rw_content_t"
tmpfiles_conf: "/etc/tmpfiles.d/upload-logger.conf"
logrotate_dest: "/etc/logrotate.d/upload-logger"
tasks:
- name: Ensure logs directory exists
file:
path: "{{ logs_dir }}"
state: directory
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "{{ logs_perms }}"
- name: Ensure quarantine directory exists
file:
path: "{{ quarantine_dir }}"
state: directory
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "{{ quarantine_perms }}"
- name: Ensure state directory exists
file:
path: "{{ state_dir }}"
state: directory
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "{{ state_perms }}"
- name: Ensure example upload-logger.json is copied (only when missing)
copy:
src: "{{ examples_dir }}/upload-logger.json"
dest: "{{ upload_logger_root }}/upload-logger.json"
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "0644"
when: not (upload_logger_root + '/upload-logger.json') | path_exists
- name: Install tmpfiles.d entry to recreate dirs at boot
copy:
dest: "{{ tmpfiles_conf }}"
content: |
d {{ quarantine_dir }} {{ quarantine_perms }} {{ quarantine_owner }} {{ quarantine_group }} -
d {{ state_dir }} {{ state_perms }} {{ quarantine_owner }} {{ quarantine_group }} -
owner: root
group: root
mode: '0644'
- name: Install logrotate snippet if example exists
copy:
src: "{{ examples_dir }}/logrotate.d/upload-logger"
dest: "{{ logrotate_dest }}"
owner: root
group: root
mode: '0644'
when: (examples_dir + '/logrotate.d/upload-logger') | path_exists
- name: Set SELinux fcontext for directories when selinux enabled
when: ansible_selinux.status == 'enabled'
sefcontext:
target: "{{ item }}(/.*)?"
setype: "{{ selinux_fcontext }}"
loop:
- "{{ quarantine_dir }}"
- "{{ state_dir }}"
- "{{ logs_dir }}"
- name: Apply SELinux contexts
when: ansible_selinux.status == 'enabled'
command: restorecon -Rv {{ quarantine_dir }} {{ state_dir }} {{ logs_dir }}
- name: Ensure log file exists with correct mode (touch)
file:
path: "{{ logs_dir }}/uploads.log"
state: touch
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "{{ log_file_mode }}"
- name: Summary - show directories
debug:
msg: |
Provisioned:
- logs: {{ logs_dir }} (owner={{ quarantine_owner }} group={{ quarantine_group }} mode={{ logs_perms }})
- quarantine: {{ quarantine_dir }} (owner={{ quarantine_owner }} group={{ quarantine_group }} mode={{ quarantine_perms }})
- state: {{ state_dir }} (owner={{ quarantine_owner }} group={{ quarantine_group }} mode={{ state_perms }})

View File

@@ -0,0 +1,63 @@
---
# Ansible playbook snippet to provision upload-logger directories and permissions.
# Usage: ansible-playbook -i inventory scripts/ansible/upload-logger-provision.yml
- hosts: web
become: true
vars:
upload_logger_root: "{{ playbook_dir | default('.') | dirname | realpath }}"
quarantine_dir: "{{ upload_logger_root }}/quarantine"
state_dir: "{{ upload_logger_root }}/state"
quarantine_owner: "root"
quarantine_group: "www-data"
quarantine_perms: "0700"
state_perms: "0750"
selinux_fcontext: "httpd_sys_rw_content_t"
tasks:
- name: Ensure quarantine directory exists
file:
path: "{{ quarantine_dir }}"
state: directory
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "{{ quarantine_perms }}"
- name: Ensure state directory exists
file:
path: "{{ state_dir }}"
state: directory
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
mode: "{{ state_perms }}"
- name: Ensure quarantined files have strict permissions (files -> 0600)
find:
paths: "{{ quarantine_dir }}"
file_type: file
register: quarantine_files
- name: Set strict mode on existing quarantined files
file:
path: "{{ item.path }}"
mode: '0600'
owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}"
loop: "{{ quarantine_files.files }}"
when: quarantine_files.matched > 0
- name: Set SELinux fcontext for quarantine dir (when selinux enabled)
when: ansible_selinux.status == 'enabled'
sefcontext:
target: "{{ quarantine_dir }}(/.*)?"
setype: "{{ selinux_fcontext }}"
- name: Set SELinux fcontext for state dir (when selinux enabled)
when: ansible_selinux.status == 'enabled'
sefcontext:
target: "{{ state_dir }}(/.*)?"
setype: "{{ selinux_fcontext }}"
- name: Apply SELinux contexts
when: ansible_selinux.status == 'enabled'
command: restorecon -Rv {{ quarantine_dir }} {{ state_dir }}

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Wrapper to provision upload-logger directories using Ansible if available,
# otherwise falling back to the included provision_dirs.sh script.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
ANSIBLE_PLAYBOOK="$(command -v ansible-playbook || true)"
PLAYBOOK_PATH="$ROOT_DIR/scripts/ansible/provision-full.yml"
FALLBACK_SCRIPT="$ROOT_DIR/scripts/provision_dirs.sh"
if [[ -n "$ANSIBLE_PLAYBOOK" && -f "$PLAYBOOK_PATH" ]]; then
echo "Running Ansible playbook: $PLAYBOOK_PATH"
# Use local connection if running on the target host
if [[ "$1" == "local" ]]; then
sudo ansible-playbook -i localhost, -c local "$PLAYBOOK_PATH"
else
sudo ansible-playbook "$PLAYBOOK_PATH"
fi
else
echo "Ansible not available or playbook missing; using fallback script"
sudo "$FALLBACK_SCRIPT" "$@"
fi

104
scripts/provision_dirs.sh Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -euo pipefail
# Provision quarantine and state directories for upload-logger
# Usage: sudo ./provision_dirs.sh [--config path/to/upload-logger.json]
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CFG="${1:-$ROOT_DIR/upload-logger.json}"
QUIET=0
if [[ "${2:-}" == "--quiet" ]]; then QUIET=1; fi
info(){ if [[ $QUIET -ne 1 ]]; then echo "[INFO] $*"; fi }
err(){ echo "[ERROR] $*" >&2; }
if [[ $EUID -ne 0 ]]; then
err "This script must be run as root to set ownership and SELinux contexts."
err "Rerun as: sudo $0"
exit 2
fi
# Defaults
QUARANTINE_DIR_DEFAULT="$ROOT_DIR/quarantine"
STATE_DIR_DEFAULT="$ROOT_DIR/state"
QUARANTINE_OWNER_DEFAULT="root"
QUARANTINE_GROUP_DEFAULT="www-data"
QUARANTINE_PERMS_DEFAULT="0700"
STATE_PERMS_DEFAULT="0750"
SELINUX_FCONTEXT_DEFAULT="httpd_sys_rw_content_t"
QUARANTINE_DIR="$QUARANTINE_DIR_DEFAULT"
STATE_DIR="$STATE_DIR_DEFAULT"
QUARANTINE_OWNER="$QUARANTINE_OWNER_DEFAULT"
QUARANTINE_GROUP="$QUARANTINE_GROUP_DEFAULT"
QUARANTINE_PERMS="$QUARANTINE_PERMS_DEFAULT"
STATE_PERMS="$STATE_PERMS_DEFAULT"
SELINUX_FCONTEXT="$SELINUX_FCONTEXT_DEFAULT"
if [[ -f "$CFG" ]]; then
info "Loading config from $CFG"
# Use jq-like parsing with grep/sed to avoid requiring jq on systems
QUARANTINE_DIR=$(grep -oP '"quarantine_dir"\s*:\s*"\K[^"]+' "$CFG" || echo "$QUARANTINE_DIR")
STATE_DIR=$(grep -oP '"state_dir"\s*:\s*"\K[^"]+' "$CFG" || echo "$STATE_DIR")
QUARANTINE_PERMS=$(grep -oP '"quarantine_dir_perms"\s*:\s*"\K[^"]+' "$CFG" || echo "$QUARANTINE_PERMS")
QUARANTINE_OWNER=$(grep -oP '"quarantine_owner"\s*:\s*"\K[^"]+' "$CFG" || echo "$QUARANTINE_OWNER")
QUARANTINE_GROUP=$(grep -oP '"quarantine_group"\s*:\s*"\K[^"]+' "$CFG" || echo "$QUARANTINE_GROUP")
fi
info "Ensuring directories exist"
mkdir -p -- "$QUARANTINE_DIR"
mkdir -p -- "$STATE_DIR"
info "Setting permissions and ownership"
chmod ${QUARANTINE_PERMS} "$QUARANTINE_DIR" || true
chown ${QUARANTINE_OWNER}:${QUARANTINE_GROUP} "$QUARANTINE_DIR" || true
chmod ${STATE_PERMS} "$STATE_DIR" || true
chown ${QUARANTINE_OWNER}:${QUARANTINE_GROUP} "$STATE_DIR" || true
# Ensure existing files in quarantine are not world-readable/executable.
if [[ -d "$QUARANTINE_DIR" ]]; then
info "Hardening existing files in $QUARANTINE_DIR"
# Set files to 0600 and directories to 0700
find "$QUARANTINE_DIR" -type d -print0 | xargs -0 -r chmod 0700 || true
find "$QUARANTINE_DIR" -type f -print0 | xargs -0 -r chmod 0600 || true
chown -R ${QUARANTINE_OWNER}:${QUARANTINE_GROUP} "$QUARANTINE_DIR" || true
fi
info "Verifying permissions"
ls -ld "$QUARANTINE_DIR" "$STATE_DIR"
# SELinux handling (best-effort)
if command -v getenforce >/dev/null 2>&1 && [[ "$(getenforce)" != "Disabled" ]]; then
info "SELinux enabled on system; attempting to configure file context"
if command -v semanage >/dev/null 2>&1; then
info "Registering fcontext for $QUARANTINE_DIR -> $SELINUX_FCONTEXT"
semanage fcontext -a -t ${SELINUX_FCONTEXT} "${QUARANTINE_DIR}(/.*)?" || true
info "Registering fcontext for $STATE_DIR -> ${SELINUX_FCONTEXT}"
semanage fcontext -a -t ${SELINUX_FCONTEXT} "${STATE_DIR}(/.*)?" || true
info "Applying contexts with restorecon"
restorecon -Rv "$QUARANTINE_DIR" "$STATE_DIR" || true
else
info "semanage not available; skipping fcontext registration. Install policycoreutils-python-utils or provide manual guidance."
fi
else
info "SELinux not enabled or getenforce unavailable; skipping SELinux steps"
fi
# Optional tmpfiles.d entry to recreate directories at boot (idempotent)
TMPFILE="/etc/tmpfiles.d/upload-logger.conf"
if [[ -w /etc/tmpfiles.d || $QUIET -eq 1 ]]; then
info "Writing tmpfiles.d entry to ${TMPFILE}"
cat > "$TMPFILE" <<EOF
d ${QUARANTINE_DIR} ${QUARANTINE_PERMS} ${QUARANTINE_OWNER} ${QUARANTINE_GROUP} -
d ${STATE_DIR} ${STATE_PERMS} ${QUARANTINE_OWNER} ${QUARANTINE_GROUP} -
EOF
else
info "Skipping tmpfiles.d entry (no permission to write /etc/tmpfiles.d)"
fi
info "Provisioning complete. Ensure PHP-FPM worker user can write to the state directory if needed."
echo
info "Summary:"
stat -c "%U:%G %a %n" "$QUARANTINE_DIR" "$STATE_DIR" || true
exit 0

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Controlled rollout helper to enable blocking mode by swapping in a blocking config.
# Usage: sudo ./scripts/rollout_enable_blocking.sh [--dry-run] [--confirm]
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
ACTIVE_CFG="$ROOT_DIR/upload-logger.json"
PROD_CFG="$ROOT_DIR/config/upload-logger.prod.json"
BLOCK_CFG="$ROOT_DIR/config/upload-logger.blocking.json"
BACKUP_DIR="$ROOT_DIR/config/backups"
DRY_RUN=0
CONFIRM=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--confirm) CONFIRM=1 ;;
-h|--help)
echo "Usage: $0 [--dry-run] [--confirm]"
exit 0 ;;
esac
done
if [[ ! -f "$BLOCK_CFG" ]]; then
echo "Blocking config not found: $BLOCK_CFG" >&2
exit 2
fi
if [[ ! -f "$PROD_CFG" ]]; then
echo "Prod config not found: $PROD_CFG" >&2
exit 2
fi
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY RUN: Would replace $ACTIVE_CFG with $BLOCK_CFG"
echo "DRY RUN: Would reload PHP-FPM (if present)"
exit 0
fi
if [[ $CONFIRM -ne 1 ]]; then
echo "This will replace $ACTIVE_CFG with the blocking config and reload PHP-FPM."
echo "Run with --confirm to proceed, or --dry-run to preview."
exit 1
fi
mkdir -p "$BACKUP_DIR"
TS=$(date +%Y%m%dT%H%M%S)
if [[ -f "$ACTIVE_CFG" ]]; then
cp -a "$ACTIVE_CFG" "$BACKUP_DIR/upload-logger.json.bak.$TS"
echo "Backed up current config to $BACKUP_DIR/upload-logger.json.bak.$TS"
fi
cp -a "$BLOCK_CFG" "$ACTIVE_CFG"
echo "Copied blocking config to $ACTIVE_CFG"
# Try to reload PHP-FPM gracefully using common service names
RELOADED=0
if command -v systemctl >/dev/null 2>&1; then
for svc in php-fpm php7.4-fpm php8.0-fpm php8.1-fpm php8.2-fpm; do
if systemctl list-units --full -all | grep -q "^${svc}\.service"; then
echo "Reloading $svc"
systemctl reload "$svc" || systemctl restart "$svc"
RELOADED=1
break
fi
done
fi
if [[ $RELOADED -eq 0 ]]; then
if command -v service >/dev/null 2>&1; then
for svc in php7.4-fpm php8.0-fpm php8.1-fpm php8.2-fpm php-fpm; do
if service --status-all 2>&1 | grep -q "$svc"; then
echo "Reloading $svc via service"
service "$svc" reload || service "$svc" restart
RELOADED=1
break
fi
done
fi
fi
if [[ $RELOADED -eq 0 ]]; then
echo "Warning: could not detect PHP-FPM service to reload. Please reload PHP-FPM manually."
else
echo "PHP-FPM reloaded; blocking config is active."
fi
echo "Rollout complete. Monitor logs and be ready to rollback if necessary."

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Upload Logger provisioning (one-shot)
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/upload-logger-provision.sh /opt/upload-logger/upload-logger.json
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target