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,
|
||||
# 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
|
||||
# <HOSTNAME>_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 <user> && smbpasswd -a <user>
|
||||
#
|
||||
# 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=<LUKS_KEY_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=<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:
|
||||
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -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=<this value> (use ansible-vault for production).
|
||||
# ── Ansipa SMB shares ─────────────────────────────────────────────────────────
|
||||
# SMB_SCAN_PASSWORD — password for 'scanupload'; deploy to clients via Ansible
|
||||
# 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!
|
||||
LUKS_KEY_UPLOAD_PASSWORD=ChangeMe_LuksUpload!
|
||||
|
||||
# ── Keycloak ──────────────────────────────────────────────────────────────────
|
||||
KC_HOSTNAME=keycloak.corp.example.com
|
||||
|
|
|
|||
|
|
@ -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 <user>
|
||||
# smbpasswd -a <user>
|
||||
|
||||
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" <<CONF
|
|||
create mask = 0644
|
||||
directory mask = 0755
|
||||
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
|
||||
|
||||
# ── Samba password (idempotent — smbpasswd -a adds or updates) ────────────────
|
||||
log "Setting Samba password for $SMB_USER"
|
||||
printf '%s\n%s\n' "$SMB_PASS" "$SMB_PASS" | smbpasswd -a -s "$SMB_USER" 2>/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
|
||||
|
|
|
|||
Loading…
Reference in New Issue