feat(ansipa): store LUKS backup keys on SMB share with KeyAdmin access control
ansipa-smb-setup.sh: - Adds KeyAdmin Linux group and luks-upload service account (member of KeyAdmin) on the IPA container, both persisted across restarts. - LUKS base dir /data/luks-keys owned root:KeyAdmin, mode 2750 (setgid so new files inherit the group). - New [ansipa-luks-keys] SMB share: valid users = @KeyAdmin, read only, write list = luks-upload. Human admins gain read access by being added to KeyAdmin: useradd -r -G KeyAdmin <user> && smbpasswd -a <user>. - LUKS_KEY_UPLOAD_PASSWORD sourced from env / /data/samba/ansipa-smb.env alongside the existing SMB_SCAN_PASSWORD. collect-luks-keys.yml: - After fetching /_LUKS_BACKUP_KEY from each client, uploads it to the ansipa-luks-keys share via smbclient using a temp credentials file (no_log, deleted in post_tasks). - Local staging copy is removed after a successful upload. - SMB credentials file uses an epoch-stamped path to avoid collisions. .env.example: documents LUKS_KEY_UPLOAD_PASSWORD. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
aced2c754e
commit
5d56984e38
|
|
@ -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,
|
# Flow per host:
|
||||||
# a backup LUKS key is stored at /_LUKS_BACKUP_KEY inside the encrypted root.
|
# 1. Fetch /_LUKS_BACKUP_KEY from the client to a local staging directory.
|
||||||
# This playbook fetches those keys to the controller and names each copy
|
# 2. Upload the staged file to //IPA_SERVER/ansipa-luks-keys/ via smbclient.
|
||||||
# <HOSTNAME>_LUKS_BACKUP_KEY so they can be archived securely.
|
# 3. Delete the local staging copy.
|
||||||
#
|
#
|
||||||
# Keys are stored in luks-keys/ relative to the playbook directory.
|
# The ansipa-luks-keys SMB share is write-only for 'luks-upload' and read-only
|
||||||
# Protect that directory carefully — keys can unlock client root partitions.
|
# for members of the 'KeyAdmin' group. Add a Samba user to KeyAdmin on the IPA
|
||||||
|
# container to grant read access:
|
||||||
|
# useradd -r -G KeyAdmin <user> && smbpasswd -a <user>
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ansible-playbook -i inventory collect-luks-keys.yml
|
# ansible-playbook -i inventory collect-luks-keys.yml \
|
||||||
# ansible-playbook -i inventory collect-luks-keys.yml -e luks_keys_store=/secure/path
|
# -e luks_smb_server=ipa.corp.example.com \
|
||||||
|
# -e luks_upload_password=<LUKS_KEY_UPLOAD_PASSWORD>
|
||||||
#
|
#
|
||||||
# To run automatically, add a cron job on the Ansible controller:
|
# Or set defaults in group_vars / ansible-vault. The smb_server can also be
|
||||||
# 0 3 * * * cd /path/to/playbooks && ansible-playbook -i inventory collect-luks-keys.yml
|
# 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
|
hosts: all
|
||||||
become: yes
|
become: yes
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
luks_key_path: /_LUKS_BACKUP_KEY
|
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=<IPA host>') }}"
|
||||||
|
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:
|
tasks:
|
||||||
|
|
||||||
- name: Ensure local key store directory exists
|
- name: Ensure local staging directory exists
|
||||||
file:
|
file:
|
||||||
path: "{{ luks_keys_store }}"
|
path: "{{ luks_keys_stage }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0700'
|
mode: '0700'
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
run_once: true
|
run_once: true
|
||||||
become: false
|
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
|
- name: Check for LUKS backup key on client
|
||||||
stat:
|
stat:
|
||||||
path: "{{ luks_key_path }}"
|
path: "{{ luks_key_path }}"
|
||||||
register: luks_key_stat
|
register: luks_key_stat
|
||||||
|
|
||||||
- name: Fetch LUKS backup key to controller
|
- name: Fetch LUKS backup key to local staging area
|
||||||
fetch:
|
fetch:
|
||||||
src: "{{ luks_key_path }}"
|
src: "{{ luks_key_path }}"
|
||||||
dest: "{{ luks_keys_store }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY"
|
dest: "{{ luks_keys_stage }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY"
|
||||||
flat: yes
|
flat: yes
|
||||||
when: luks_key_stat.stat.exists
|
when: luks_key_stat.stat.exists
|
||||||
register: luks_key_fetch
|
register: luks_key_fetch
|
||||||
|
|
||||||
- name: Secure fetched key permissions
|
- name: Secure staged key permissions
|
||||||
file:
|
file:
|
||||||
path: "{{ luks_keys_store }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY"
|
path: "{{ luks_keys_stage }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY"
|
||||||
mode: '0400'
|
mode: '0400'
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
become: false
|
become: false
|
||||||
|
|
@ -58,12 +82,49 @@
|
||||||
- luks_key_stat.stat.exists
|
- luks_key_stat.stat.exists
|
||||||
- luks_key_fetch is changed
|
- 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
|
- name: Report key status
|
||||||
debug:
|
debug:
|
||||||
msg: >-
|
msg: >-
|
||||||
{{ inventory_hostname }}:
|
{{ inventory_hostname }}:
|
||||||
{% if luks_key_stat.stat.exists %}
|
{% if not luks_key_stat.stat.exists %}
|
||||||
key found and fetched to {{ luks_keys_store }}/{{ inventory_hostname }}_LUKS_BACKUP_KEY
|
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 %}
|
{% else %}
|
||||||
no /_LUKS_BACKUP_KEY present (unencrypted or already collected)
|
WARNING: key fetched but SMB upload failed — check smbclient output
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
post_tasks:
|
||||||
|
- name: Remove temporary SMB credentials file
|
||||||
|
file:
|
||||||
|
path: "{{ _smb_creds_file }}"
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
run_once: true
|
||||||
|
become: false
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,17 @@ IPA_SETUP_DNS=false
|
||||||
IPA_DNS_FORWARDER=
|
IPA_DNS_FORWARDER=
|
||||||
IPA_SETUP_KRA=false
|
IPA_SETUP_KRA=false
|
||||||
|
|
||||||
# ── Ansipa SMB scan-results share ─────────────────────────────────────────────
|
# ── Ansipa SMB shares ─────────────────────────────────────────────────────────
|
||||||
# Password for the 'scanupload' Samba user. Deploy to clients via Ansible with
|
# SMB_SCAN_PASSWORD — password for 'scanupload'; deploy to clients via Ansible
|
||||||
# smb_scan_password=<this value> (use ansible-vault for production).
|
# with smb_scan_password=<this value> (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=<this value>.
|
||||||
|
# To grant read access, add a Samba user to KeyAdmin on the
|
||||||
|
# container: useradd -r -G KeyAdmin <user> && smbpasswd -a <user>
|
||||||
SMB_SCAN_PASSWORD=ChangeMe_ScanPass!
|
SMB_SCAN_PASSWORD=ChangeMe_ScanPass!
|
||||||
|
LUKS_KEY_UPLOAD_PASSWORD=ChangeMe_LuksUpload!
|
||||||
|
|
||||||
# ── Keycloak ──────────────────────────────────────────────────────────────────
|
# ── Keycloak ──────────────────────────────────────────────────────────────────
|
||||||
KC_HOSTNAME=keycloak.corp.example.com
|
KC_HOSTNAME=keycloak.corp.example.com
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,84 @@
|
||||||
#!/bin/bash
|
#!/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
|
# 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):
|
# Password sources (first match wins per variable):
|
||||||
# 1. SMB_SCAN_PASSWORD environment variable (first boot / explicit override)
|
# 1. Environment variable (first boot / explicit override)
|
||||||
# 2. /data/samba/ansipa-smb.env (persisted from first boot)
|
# 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 <user>
|
||||||
|
# smbpasswd -a <user>
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
LOG_TAG="ansipa-smb-setup"
|
LOG_TAG="ansipa-smb-setup"
|
||||||
SCAN_BASE="/data/scan-results"
|
SCAN_BASE="/data/scan-results"
|
||||||
|
LUKS_BASE="/data/luks-keys"
|
||||||
SMB_CONF="/etc/samba/smb.conf"
|
SMB_CONF="/etc/samba/smb.conf"
|
||||||
SMB_USER="scanupload"
|
SMB_USER="scanupload"
|
||||||
|
LUKS_UPLOAD_USER="luks-upload"
|
||||||
|
KEYADMIN_GROUP="KeyAdmin"
|
||||||
ENV_FILE="/data/samba/ansipa-smb.env"
|
ENV_FILE="/data/samba/ansipa-smb.env"
|
||||||
|
|
||||||
log() { echo "[$LOG_TAG] $*"; }
|
log() { echo "[$LOG_TAG] $*"; }
|
||||||
die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }
|
die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }
|
||||||
|
|
||||||
# ── Resolve password ──────────────────────────────────────────────────────────
|
# ── Resolve passwords ─────────────────────────────────────────────────────────
|
||||||
SMB_PASS="${SMB_SCAN_PASSWORD:-}"
|
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
|
# shellcheck source=/dev/null
|
||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
SMB_PASS="${SMB_SCAN_PASSWORD:-}"
|
SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}"
|
||||||
|
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}"
|
||||||
fi
|
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 ──────────────────────────────────────────
|
# ── Persist for subsequent restarts ──────────────────────────────────────────
|
||||||
# %q shell-quotes the value so passwords with spaces or special chars are safe.
|
|
||||||
mkdir -p "$(dirname "$ENV_FILE")"
|
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"
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
# ── Directory structure (idempotent) ──────────────────────────────────────────
|
# ── Directory structure (idempotent) ──────────────────────────────────────────
|
||||||
mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts"
|
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
|
if ! id "$SMB_USER" &>/dev/null; then
|
||||||
useradd -r -s /sbin/nologin -d "$SCAN_BASE" -M "$SMB_USER"
|
useradd -r -s /sbin/nologin -d "$SCAN_BASE" -M "$SMB_USER"
|
||||||
log "Created system user: $SMB_USER"
|
log "Created system user: $SMB_USER"
|
||||||
fi
|
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 "$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 ──────────────────────────────────────────────────────────────────
|
# ── smb.conf ──────────────────────────────────────────────────────────────────
|
||||||
log "Writing $SMB_CONF"
|
log "Writing $SMB_CONF"
|
||||||
|
|
@ -73,13 +107,29 @@ cat > "$SMB_CONF" <<CONF
|
||||||
create mask = 0644
|
create mask = 0644
|
||||||
directory mask = 0755
|
directory mask = 0755
|
||||||
force user = $SMB_USER
|
force user = $SMB_USER
|
||||||
|
|
||||||
|
[ansipa-luks-keys]
|
||||||
|
comment = Ansipa LUKS backup keys — KeyAdmin read, luks-upload write only
|
||||||
|
path = $LUKS_BASE
|
||||||
|
valid users = @$KEYADMIN_GROUP
|
||||||
|
read only = yes
|
||||||
|
write list = $LUKS_UPLOAD_USER
|
||||||
|
browseable = no
|
||||||
|
create mask = 0640
|
||||||
|
directory mask = 0750
|
||||||
CONF
|
CONF
|
||||||
|
|
||||||
# ── Samba password (idempotent — smbpasswd -a adds or updates) ────────────────
|
# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ───────────────
|
||||||
log "Setting Samba password for $SMB_USER"
|
_smb_set_pass() {
|
||||||
printf '%s\n%s\n' "$SMB_PASS" "$SMB_PASS" | smbpasswd -a -s "$SMB_USER" 2>/dev/null || \
|
local user="$1" pass="$2"
|
||||||
printf '%s\n%s\n' "$SMB_PASS" "$SMB_PASS" | smbpasswd -s "$SMB_USER" 2>/dev/null || \
|
log "Setting Samba password for $user"
|
||||||
log "WARN: smbpasswd returned non-zero (user may already exist with correct password)"
|
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) ─
|
# ── 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
|
# Always (re-)write: /etc/cron.d is on the ephemeral container layer and is
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue