diff --git a/setup/modules/FreeipaAnsible/ansible/collect-luks-keys.yml b/setup/modules/FreeipaAnsible/ansible/collect-luks-keys.yml index ff986e3..fd2ebf3 100644 --- a/setup/modules/FreeipaAnsible/ansible/collect-luks-keys.yml +++ b/setup/modules/FreeipaAnsible/ansible/collect-luks-keys.yml @@ -1,56 +1,80 @@ --- -# collect-luks-keys.yml — fetch LUKS backup keys from enrolled clients. +# collect-luks-keys.yml — fetch LUKS backup keys from enrolled clients and store them +# on the ansipa-luks-keys SMB share (accessible only to KeyAdmin group members). # -# When a client was installed with disk encryption via the M-Archy installer, -# a backup LUKS key is stored at /_LUKS_BACKUP_KEY inside the encrypted root. -# This playbook fetches those keys to the controller and names each copy -# _LUKS_BACKUP_KEY so they can be archived securely. +# Flow per host: +# 1. Fetch /_LUKS_BACKUP_KEY from the client to a local staging directory. +# 2. Upload the staged file to //IPA_SERVER/ansipa-luks-keys/ via smbclient. +# 3. Delete the local staging copy. # -# Keys are stored in luks-keys/ relative to the playbook directory. -# Protect that directory carefully — keys can unlock client root partitions. +# The ansipa-luks-keys SMB share is write-only for 'luks-upload' and read-only +# for members of the 'KeyAdmin' group. Add a Samba user to KeyAdmin on the IPA +# container to grant read access: +# useradd -r -G KeyAdmin && smbpasswd -a # # Usage: -# ansible-playbook -i inventory collect-luks-keys.yml -# ansible-playbook -i inventory collect-luks-keys.yml -e luks_keys_store=/secure/path +# ansible-playbook -i inventory collect-luks-keys.yml \ +# -e luks_smb_server=ipa.corp.example.com \ +# -e luks_upload_password= # -# To run automatically, add a cron job on the Ansible controller: -# 0 3 * * * cd /path/to/playbooks && ansible-playbook -i inventory collect-luks-keys.yml +# Or set defaults in group_vars / ansible-vault. The smb_server can also be +# auto-detected from /etc/ipa/default.conf on the clients. -- name: Collect LUKS backup keys from enrolled clients +- name: Collect and archive LUKS backup keys hosts: all become: yes vars: luks_key_path: /_LUKS_BACKUP_KEY - luks_keys_store: "{{ playbook_dir }}/luks-keys" + # Local staging dir — files are deleted after a successful SMB upload. + luks_keys_stage: "{{ playbook_dir }}/luks-keys-stage" + luks_smb_server: "{{ luks_smb_server | mandatory('luks_smb_server is required — use -e luks_smb_server=') }}" + luks_smb_share: ansipa-luks-keys + luks_upload_user: luks-upload + luks_upload_password: "{{ luks_upload_password | mandatory('luks_upload_password is required — use -e luks_upload_password=... or ansible-vault') }}" + # Temp credentials file on the controller — removed at the end of the play. + _smb_creds_file: "/tmp/.ansipa-luks-upload-{{ ansible_date_time.epoch }}.creds" tasks: - - name: Ensure local key store directory exists + - name: Ensure local staging directory exists file: - path: "{{ luks_keys_store }}" + path: "{{ luks_keys_stage }}" state: directory mode: '0700' delegate_to: localhost run_once: true become: false + - name: Write temporary SMB credentials file on controller + copy: + dest: "{{ _smb_creds_file }}" + mode: '0600' + content: | + username = {{ luks_upload_user }} + password = {{ luks_upload_password }} + domain = WORKGROUP + delegate_to: localhost + run_once: true + become: false + no_log: true + - name: Check for LUKS backup key on client stat: path: "{{ luks_key_path }}" register: luks_key_stat - - name: Fetch LUKS backup key to controller + - name: Fetch LUKS backup key to local staging area fetch: src: "{{ luks_key_path }}" - dest: "{{ luks_keys_store }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY" + dest: "{{ luks_keys_stage }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY" flat: yes when: luks_key_stat.stat.exists register: luks_key_fetch - - name: Secure fetched key permissions + - name: Secure staged key permissions file: - path: "{{ luks_keys_store }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY" + path: "{{ luks_keys_stage }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY" mode: '0400' delegate_to: localhost become: false @@ -58,12 +82,49 @@ - luks_key_stat.stat.exists - luks_key_fetch is changed + - name: Upload key to ansipa-luks-keys SMB share + shell: > + smbclient "//{{ luks_smb_server }}/{{ luks_smb_share }}" + -A "{{ _smb_creds_file }}" + -c "put {{ luks_keys_stage }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY {{ inventory_hostname }}_LUKS_BACKUP_KEY" + delegate_to: localhost + become: false + when: + - luks_key_stat.stat.exists + - luks_key_fetch is changed + register: smb_upload + no_log: true + + - name: Remove local staging copy after successful upload + file: + path: "{{ luks_keys_stage }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY" + state: absent + delegate_to: localhost + become: false + when: + - luks_key_stat.stat.exists + - luks_key_fetch is changed + - smb_upload is succeeded + - name: Report key status debug: msg: >- {{ inventory_hostname }}: - {% if luks_key_stat.stat.exists %} - key found and fetched to {{ luks_keys_store }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY + {% if not luks_key_stat.stat.exists %} + no /_LUKS_BACKUP_KEY present (unencrypted install or key already removed) + {% elif luks_key_fetch is changed and smb_upload is succeeded %} + key uploaded to //{{ luks_smb_server }}/{{ luks_smb_share }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY + {% elif luks_key_fetch is not changed %} + key unchanged since last collection — skipped upload {% else %} - no /_LUKS_BACKUP_KEY present (unencrypted or already collected) + WARNING: key fetched but SMB upload failed — check smbclient output {% endif %} + + post_tasks: + - name: Remove temporary SMB credentials file + file: + path: "{{ _smb_creds_file }}" + state: absent + delegate_to: localhost + run_once: true + become: false diff --git a/setup/modules/FreeipaAnsible/image/.env.example b/setup/modules/FreeipaAnsible/image/.env.example index bca6ab1..d663333 100644 --- a/setup/modules/FreeipaAnsible/image/.env.example +++ b/setup/modules/FreeipaAnsible/image/.env.example @@ -8,10 +8,17 @@ IPA_SETUP_DNS=false IPA_DNS_FORWARDER= IPA_SETUP_KRA=false -# ── Ansipa SMB scan-results share ───────────────────────────────────────────── -# Password for the 'scanupload' Samba user. Deploy to clients via Ansible with -# smb_scan_password= (use ansible-vault for production). +# ── Ansipa SMB shares ───────────────────────────────────────────────────────── +# SMB_SCAN_PASSWORD — password for 'scanupload'; deploy to clients via Ansible +# with smb_scan_password= (use ansible-vault). +# LUKS_KEY_UPLOAD_PASSWORD — password for the 'luks-upload' service account used +# by the Ansible controller to write LUKS backup keys to +# the ansipa-luks-keys share. Pass to collect-luks-keys.yml +# with -e luks_upload_password=. +# To grant read access, add a Samba user to KeyAdmin on the +# container: useradd -r -G KeyAdmin && smbpasswd -a SMB_SCAN_PASSWORD=ChangeMe_ScanPass! +LUKS_KEY_UPLOAD_PASSWORD=ChangeMe_LuksUpload! # ── Keycloak ────────────────────────────────────────────────────────────────── KC_HOSTNAME=keycloak.corp.example.com diff --git a/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh index e6cf33e..bc80f15 100644 --- a/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh +++ b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh @@ -1,50 +1,84 @@ #!/bin/bash -# ansipa-smb-setup.sh — configure the Samba scan-results share on the IPA container. +# ansipa-smb-setup.sh — configure the Samba scan-results and LUKS-key shares on the IPA container. # # Runs on every container start via ansipa-smb.service so that smb.conf and -# the Samba user are always in place after container restarts (ephemeral rootfs). +# Samba users are always in place after container restarts (ephemeral rootfs). # -# Password source (first match wins): -# 1. SMB_SCAN_PASSWORD environment variable (first boot / explicit override) -# 2. /data/samba/ansipa-smb.env (persisted from first boot) +# Password sources (first match wins per variable): +# 1. Environment variable (first boot / explicit override) +# 2. /data/samba/ansipa-smb.env (persisted from first boot) +# +# Shares: +# ansipa-scans — write-only for 'scanupload'; clients push scan results here. +# ansipa-luks-keys — write-only for 'luks-upload' (Ansible controller); +# read for members of the 'KeyAdmin' Linux group. +# Add a Samba user to KeyAdmin to grant key-read access: +# useradd -r -G KeyAdmin +# smbpasswd -a set -euo pipefail LOG_TAG="ansipa-smb-setup" SCAN_BASE="/data/scan-results" +LUKS_BASE="/data/luks-keys" SMB_CONF="/etc/samba/smb.conf" SMB_USER="scanupload" +LUKS_UPLOAD_USER="luks-upload" +KEYADMIN_GROUP="KeyAdmin" ENV_FILE="/data/samba/ansipa-smb.env" log() { echo "[$LOG_TAG] $*"; } die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; } -# ── Resolve password ────────────────────────────────────────────────────────── +# ── Resolve passwords ───────────────────────────────────────────────────────── SMB_PASS="${SMB_SCAN_PASSWORD:-}" +LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}" -if [[ -z "$SMB_PASS" ]] && [[ -f "$ENV_FILE" ]]; then +if [[ -f "$ENV_FILE" ]]; then # shellcheck source=/dev/null source "$ENV_FILE" - SMB_PASS="${SMB_SCAN_PASSWORD:-}" + SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}" + LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}" fi -[[ -z "$SMB_PASS" ]] && die "SMB_SCAN_PASSWORD not set and $ENV_FILE not present. Set it in .env." +[[ -z "$SMB_PASS" ]] && die "SMB_SCAN_PASSWORD not set and $ENV_FILE not present. Set it in .env." +[[ -z "$LUKS_PASS" ]] && die "LUKS_KEY_UPLOAD_PASSWORD not set and $ENV_FILE not present. Set it in .env." # ── Persist for subsequent restarts ────────────────────────────────────────── -# %q shell-quotes the value so passwords with spaces or special chars are safe. mkdir -p "$(dirname "$ENV_FILE")" -printf 'SMB_SCAN_PASSWORD=%q\n' "$SMB_PASS" > "$ENV_FILE" +{ + printf 'SMB_SCAN_PASSWORD=%q\n' "$SMB_PASS" + printf 'LUKS_KEY_UPLOAD_PASSWORD=%q\n' "$LUKS_PASS" +} > "$ENV_FILE" chmod 600 "$ENV_FILE" # ── Directory structure (idempotent) ────────────────────────────────────────── mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts" +mkdir -p "$LUKS_BASE" -# ── System user ─────────────────────────────────────────────────────────────── +# ── KeyAdmin group ──────────────────────────────────────────────────────────── +if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then + groupadd -r "$KEYADMIN_GROUP" + log "Created group: $KEYADMIN_GROUP" +fi + +# ── System users ────────────────────────────────────────────────────────────── if ! id "$SMB_USER" &>/dev/null; then useradd -r -s /sbin/nologin -d "$SCAN_BASE" -M "$SMB_USER" log "Created system user: $SMB_USER" fi + +if ! id "$LUKS_UPLOAD_USER" &>/dev/null; then + useradd -r -s /sbin/nologin -d "$LUKS_BASE" -M -G "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER" + log "Created system user: $LUKS_UPLOAD_USER (member of $KEYADMIN_GROUP)" +else + # Ensure group membership is correct after container recreations + usermod -aG "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER" +fi + chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE" +chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE" +chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group # ── smb.conf ────────────────────────────────────────────────────────────────── log "Writing $SMB_CONF" @@ -73,13 +107,29 @@ cat > "$SMB_CONF" </dev/null || \ -printf '%s\n%s\n' "$SMB_PASS" "$SMB_PASS" | smbpasswd -s "$SMB_USER" 2>/dev/null || \ -log "WARN: smbpasswd returned non-zero (user may already exist with correct password)" +# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ─────────────── +_smb_set_pass() { + local user="$1" pass="$2" + log "Setting Samba password for $user" + printf '%s\n%s\n' "$pass" "$pass" | smbpasswd -a -s "$user" 2>/dev/null || \ + printf '%s\n%s\n' "$pass" "$pass" | smbpasswd -s "$user" 2>/dev/null || \ + log "WARN: smbpasswd returned non-zero for $user (may already be set correctly)" +} + +_smb_set_pass "$SMB_USER" "$SMB_PASS" +_smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS" # ── Server-side scan checker cron (hourly, analysed on the IPA server itself) ─ # Always (re-)write: /etc/cron.d is on the ephemeral container layer and is