Harden quarantine provisioning; enforce strict permissions and update Ansible and docs
This commit is contained in:
104
scripts/ansible/provision-full.yml
Normal file
104
scripts/ansible/provision-full.yml
Normal 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 }})
|
||||
63
scripts/ansible/upload-logger-provision.yml
Normal file
63
scripts/ansible/upload-logger-provision.yml
Normal 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 }}
|
||||
21
scripts/deploy_provision.sh
Normal file
21
scripts/deploy_provision.sh
Normal 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
104
scripts/provision_dirs.sh
Normal 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
|
||||
88
scripts/rollout_enable_blocking.sh
Normal file
88
scripts/rollout_enable_blocking.sh
Normal 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."
|
||||
11
scripts/systemd/upload-logger-provision.service
Normal file
11
scripts/systemd/upload-logger-provision.service
Normal 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
|
||||
Reference in New Issue
Block a user