Merge branch 'main' of https://git.abdelbaki.eu/The_miro/Dotfiles
commit
9f30bbe5b4
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PREFIX="ansipa-install-"
|
||||||
|
|
||||||
|
# Detect distro
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
DISTRO=$ID
|
||||||
|
else
|
||||||
|
DISTRO="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Package install function
|
||||||
|
# -----------------------------
|
||||||
|
install_pkg() {
|
||||||
|
PKG="$1"
|
||||||
|
case "$DISTRO" in
|
||||||
|
arch)
|
||||||
|
pacman -Sy --noconfirm "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
apt update -y && apt install -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
rhel|centos|rocky|almalinux)
|
||||||
|
yum install -y "$PKG" 2>/dev/null || dnf install -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
dnf install -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
opensuse*|sles)
|
||||||
|
zypper install -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Package uninstall function
|
||||||
|
# -----------------------------
|
||||||
|
remove_pkg() {
|
||||||
|
PKG="$1"
|
||||||
|
case "$DISTRO" in
|
||||||
|
arch)
|
||||||
|
pacman -Rns --noconfirm "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
apt remove -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
rhel|centos|rocky|almalinux)
|
||||||
|
yum remove -y "$PKG" 2>/dev/null || dnf remove -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
dnf remove -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
opensuse*|sles)
|
||||||
|
zypper remove -y "$PKG" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Get FreeIPA install groups
|
||||||
|
# -----------------------------
|
||||||
|
IPA_GROUPS=$(ipa group-find --pkey-only | awk '{print $1}' | grep "^$PREFIX" || true)
|
||||||
|
|
||||||
|
# Extract package names from groups
|
||||||
|
DESIRED_PKGS=()
|
||||||
|
for G in $IPA_GROUPS; do
|
||||||
|
PKG="${G#$PREFIX}"
|
||||||
|
DESIRED_PKGS+=("$PKG")
|
||||||
|
done
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Get currently installed packages
|
||||||
|
# -----------------------------
|
||||||
|
case "$DISTRO" in
|
||||||
|
arch)
|
||||||
|
INSTALLED_PKGS=$(pacman -Qq)
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
INSTALLED_PKGS=$(dpkg -l | awk '/^ii/ {print $2}')
|
||||||
|
;;
|
||||||
|
rhel|centos|rocky|almalinux|fedora)
|
||||||
|
INSTALLED_PKGS=$(rpm -qa --qf "%{NAME}\n")
|
||||||
|
;;
|
||||||
|
opensuse*|sles)
|
||||||
|
INSTALLED_PKGS=$(rpm -qa --qf "%{NAME}\n")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
INSTALLED_PKGS=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Install missing packages
|
||||||
|
# -----------------------------
|
||||||
|
for PKG in "${DESIRED_PKGS[@]}"; do
|
||||||
|
if ! echo "$INSTALLED_PKGS" | grep -qx "$PKG"; then
|
||||||
|
echo "[INFO] Installing package: $PKG"
|
||||||
|
install_pkg "$PKG"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Remove packages no longer desired
|
||||||
|
# -----------------------------
|
||||||
|
for PKG in $INSTALLED_PKGS; do
|
||||||
|
if [[ "$PKG" =~ ^.*$ ]]; then
|
||||||
|
if [[ "$PKG" == "$PREFIX"* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$PKG" | grep -q "^"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ " ${DESIRED_PKGS[*]} " != *" $PKG "* ]]; then
|
||||||
|
# Only remove packages that were installed via ansipa-install
|
||||||
|
if ipa group-find --pkey-only | grep -q "^$PREFIX$PKG$"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Removing package no longer required: $PKG"
|
||||||
|
remove_pkg "$PKG"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Install packages based on FreeIPA ansipa-install-* groups
|
||||||
|
After=network-online.target sssd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/ansipa-install-packages.sh
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Periodic FreeIPA package sync
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=30min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LOCAL_GROUP="baseusers"
|
||||||
|
|
||||||
|
# Ensure local group exists
|
||||||
|
if ! getent group "$LOCAL_GROUP" >/dev/null; then
|
||||||
|
groupadd "$LOCAL_GROUP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect active user (works for SSH + console)
|
||||||
|
CURRENT_USER=$(who | awk '{print $1}' | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$CURRENT_USER" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure user exists
|
||||||
|
if ! id "$CURRENT_USER" >/dev/null 2>&1; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if user is in FreeIPA BaseUser group
|
||||||
|
if id "$CURRENT_USER" | grep -q "BaseUser"; then
|
||||||
|
|
||||||
|
# Add to local group if missing
|
||||||
|
if ! id "$CURRENT_USER" | grep -q "$LOCAL_GROUP"; then
|
||||||
|
usermod -aG "$LOCAL_GROUP" "$CURRENT_USER"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Trigger BaseUser sync on login
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathExistsGlob=/run/user/*
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Sync FreeIPA BaseUser membership to local group
|
||||||
|
After=sssd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/auto-add-baseuser.sh
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
- name: Deploy FreeIPA package auto-installer
|
||||||
|
hosts: all
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: Install script
|
||||||
|
copy:
|
||||||
|
src: ansipa-install-packages.sh
|
||||||
|
dest: /usr/local/bin/ansipa-install-packages.sh
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Install systemd service
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/ansipa-install.service
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Install packages based on FreeIPA ansipa-install-* groups
|
||||||
|
After=network-online.target sssd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/ansipa-install-packages.sh
|
||||||
|
|
||||||
|
- name: Install systemd timer
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/ansipa-install.timer
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Periodic FreeIPA package sync
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=30min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
||||||
|
- name: Reload systemd
|
||||||
|
command: systemctl daemon-reload
|
||||||
|
|
||||||
|
- name: Enable and start timer
|
||||||
|
systemd:
|
||||||
|
name: ansipa-install.timer
|
||||||
|
enabled: yes
|
||||||
|
state: started
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
- name: Deploy BaseUser auto-group sync
|
||||||
|
hosts: all
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: Install script
|
||||||
|
copy:
|
||||||
|
src: auto-add-baseuser.sh
|
||||||
|
dest: /usr/local/bin/auto-add-baseuser.sh
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Install systemd service
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/baseuser-sync.service
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Sync FreeIPA BaseUser membership to local group
|
||||||
|
After=sssd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/auto-add-baseuser.sh
|
||||||
|
|
||||||
|
- name: Install systemd path unit
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/baseuser-sync.path
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Trigger BaseUser sync on login
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathExistsGlob=/run/user/*
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
- name: Reload systemd
|
||||||
|
command: systemctl daemon-reload
|
||||||
|
|
||||||
|
- name: Enable and start path unit
|
||||||
|
systemd:
|
||||||
|
name: baseuser-sync.path
|
||||||
|
enabled: yes
|
||||||
|
state: started
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $# -ne 3 ]; then
|
||||||
|
echo "Usage: $0 <controller-url> <api-token> <inventory-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTROLLER_URL="$1"
|
||||||
|
API_TOKEN="$2"
|
||||||
|
INVENTORY_NAME="$3"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Detect OS + environment
|
||||||
|
# -----------------------------
|
||||||
|
echo "[INFO] Detecting OS and environment..."
|
||||||
|
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
DISTRO=$ID
|
||||||
|
DISTRO_PRETTY=$PRETTY_NAME
|
||||||
|
else
|
||||||
|
DISTRO="unknown"
|
||||||
|
DISTRO_PRETTY="Unknown OS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Detect virtualization (with hardware detection)
|
||||||
|
# -----------------------------
|
||||||
|
RAW_VIRT=$(systemd-detect-virt 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
case "$RAW_VIRT" in
|
||||||
|
none)
|
||||||
|
VIRT_TYPE="hardware"
|
||||||
|
;;
|
||||||
|
docker|podman|lxc|container)
|
||||||
|
VIRT_TYPE="container"
|
||||||
|
;;
|
||||||
|
kvm|qemu|vmware|xen|microsoft|oracle|hyperv)
|
||||||
|
VIRT_TYPE="$RAW_VIRT"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
VIRT_TYPE="unknown"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Detect cloud provider
|
||||||
|
# -----------------------------
|
||||||
|
if curl -s --connect-timeout 1 http://169.254.169.254/latest/meta-data/ >/dev/null; then
|
||||||
|
CLOUD="aws"
|
||||||
|
elif curl -s --connect-timeout 1 -H Metadata:true http://169.254.169.254/metadata/instance?api-version=2021-02-01 >/dev/null; then
|
||||||
|
CLOUD="azure"
|
||||||
|
elif curl -s --connect-timeout 1 http://metadata.google.internal >/dev/null; then
|
||||||
|
CLOUD="gcp"
|
||||||
|
else
|
||||||
|
CLOUD="none"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IPA_HOSTNAME=$(hostname -f)
|
||||||
|
|
||||||
|
echo "[INFO] Hostname: $IPA_HOSTNAME"
|
||||||
|
echo "[INFO] OS: $DISTRO_PRETTY"
|
||||||
|
echo "[INFO] Architecture: $ARCH"
|
||||||
|
echo "[INFO] Virtualization: $VIRT_TYPE"
|
||||||
|
echo "[INFO] Cloud provider: $CLOUD"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Install Python
|
||||||
|
# -----------------------------
|
||||||
|
install_python() {
|
||||||
|
case "$DISTRO" in
|
||||||
|
arch)
|
||||||
|
sudo pacman -Sy --noconfirm python
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
sudo apt update && sudo apt install -y python3
|
||||||
|
;;
|
||||||
|
rhel|centos|rocky|almalinux)
|
||||||
|
sudo yum install -y python3 || sudo dnf install -y python3
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
sudo dnf install -y python3
|
||||||
|
;;
|
||||||
|
opensuse*|sles)
|
||||||
|
sudo zypper install -y python3
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[WARN] Unknown distro, trying generic python3 install..."
|
||||||
|
sudo bash -c "apt install -y python3 || yum install -y python3 || dnf install -y python3 || pacman -Sy --noconfirm python"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
echo "[INFO] Installing Python..."
|
||||||
|
install_python
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Ensure SSH + GSSAPI
|
||||||
|
# -----------------------------
|
||||||
|
echo "[INFO] Ensuring SSH is running..."
|
||||||
|
sudo systemctl enable sshd --now 2>/dev/null || sudo systemctl enable ssh --now || true
|
||||||
|
|
||||||
|
if ! grep -q "GSSAPIAuthentication yes" /etc/ssh/sshd_config; then
|
||||||
|
echo "GSSAPIAuthentication yes" | sudo tee -a /etc/ssh/sshd_config
|
||||||
|
sudo systemctl restart sshd || sudo systemctl restart ssh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Helper: API GET
|
||||||
|
# -----------------------------
|
||||||
|
api_get() {
|
||||||
|
curl -s -H "Authorization: Bearer $API_TOKEN" "$CONTROLLER_URL$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Helper: API POST
|
||||||
|
# -----------------------------
|
||||||
|
api_post() {
|
||||||
|
curl -s -X POST -H "Authorization: Bearer $API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$2" "$CONTROLLER_URL$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Ensure inventory exists
|
||||||
|
# -----------------------------
|
||||||
|
echo "[INFO] Checking inventory '$INVENTORY_NAME'..."
|
||||||
|
|
||||||
|
INVENTORY_ID=$(api_get "/api/v2/inventories/?name=$INVENTORY_NAME" \
|
||||||
|
| python3 -c "import sys, json; d=json.load(sys.stdin); print(d['results'][0]['id'] if d['count'] else '')")
|
||||||
|
|
||||||
|
if [ -z "$INVENTORY_ID" ]; then
|
||||||
|
echo "[INFO] Inventory not found. Creating..."
|
||||||
|
INVENTORY_ID=$(api_post "/api/v2/inventories/" \
|
||||||
|
"{\"name\": \"$INVENTORY_NAME\", \"organization\": 1}" \
|
||||||
|
| python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
|
||||||
|
else
|
||||||
|
echo "[INFO] Inventory exists with ID $INVENTORY_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Ensure groups exist
|
||||||
|
# -----------------------------
|
||||||
|
create_group_if_missing() {
|
||||||
|
local GROUP="$1"
|
||||||
|
local GROUP_ID=$(api_get "/api/v2/groups/?name=$GROUP&inventory=$INVENTORY_ID" \
|
||||||
|
| python3 -c "import sys, json; d=json.load(sys.stdin); print(d['results'][0]['id'] if d['count'] else '')")
|
||||||
|
|
||||||
|
if [ -z "$GROUP_ID" ]; then
|
||||||
|
echo "[INFO] Creating group: $GROUP"
|
||||||
|
GROUP_ID=$(api_post "/api/v2/groups/" \
|
||||||
|
"{\"name\": \"$GROUP\", \"inventory\": $INVENTORY_ID}" \
|
||||||
|
| python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$GROUP_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
GROUP_OS=$(create_group_if_missing "$DISTRO")
|
||||||
|
GROUP_ARCH=$(create_group_if_missing "arch-$ARCH")
|
||||||
|
GROUP_VIRT=$(create_group_if_missing "virt-$VIRT_TYPE")
|
||||||
|
GROUP_CLOUD=$(create_group_if_missing "cloud-$CLOUD")
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Register host
|
||||||
|
# -----------------------------
|
||||||
|
echo "[INFO] Registering host..."
|
||||||
|
|
||||||
|
HOST_ID=$(api_post "/api/v2/hosts/" \
|
||||||
|
"{\"name\": \"$IPA_HOSTNAME\", \"inventory\": $INVENTORY_ID, \"enabled\": true}" \
|
||||||
|
| python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
|
||||||
|
|
||||||
|
echo "[INFO] Host ID: $HOST_ID"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Add host to groups
|
||||||
|
# -----------------------------
|
||||||
|
for G in $GROUP_OS $GROUP_ARCH $GROUP_VIRT $GROUP_CLOUD; do
|
||||||
|
echo "[INFO] Adding host to group ID $G"
|
||||||
|
api_post "/api/v2/groups/$G/hosts/" "{\"id\": $HOST_ID}" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[SUCCESS] Host fully enrolled and auto-classified."
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,637 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# freeipa-enroll.sh — Enroll a Linux host into FreeIPA
|
||||||
|
# Usage: sudo ./freeipa-enroll.sh [options]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -d, --domain IPA domain (e.g. corp.example.com)
|
||||||
|
# -r, --realm Kerberos realm (e.g. CORP.EXAMPLE.COM) [optional, derived from domain]
|
||||||
|
# -s, --server IPA server hostname (e.g. ipa01.corp.example.com)
|
||||||
|
# -h, --hostname This host's FQDN [optional, uses current hostname]
|
||||||
|
# -p, --principal Admin principal [default: admin]
|
||||||
|
# -w, --password Admin password (or use IPA_PASSWORD env var)
|
||||||
|
# -m, --mkhomedir Enable home directory creation on login [default: true]
|
||||||
|
# --no-mkhomedir Disable home directory creation
|
||||||
|
# --sudo Configure sudo rules via SSSD [default: true]
|
||||||
|
# --no-sudo Disable sudo via SSSD
|
||||||
|
# --no-dns-update Skip DNS record update
|
||||||
|
# --ntp-server Custom NTP server [optional]
|
||||||
|
# --fido2 Enable FIDO2 key authentication via PAM
|
||||||
|
# --fido2-user IPA username to register FIDO2 credential for (repeatable)
|
||||||
|
# --uninstall Remove FreeIPA client
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─── Colours ────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||||
|
info() { echo -e "${CYAN}[i]${NC} $*"; }
|
||||||
|
section() { echo -e "\n${BLUE}━━━ $* ━━━${NC}"; }
|
||||||
|
|
||||||
|
# ─── Defaults ───────────────────────────────────────────────────────────────
|
||||||
|
IPA_DOMAIN="freeipa.abdelbaki.eu"
|
||||||
|
IPA_REALM="FREEIPA.ABDELBAKI.EU"
|
||||||
|
IPA_SERVER="freeipa.abdelbaki.eu"
|
||||||
|
IPA_HOSTNAME=""
|
||||||
|
IPA_PRINCIPAL="admin"
|
||||||
|
IPA_PASSWORD="${IPA_PASSWORD:-}"
|
||||||
|
MKHOMEDIR=true
|
||||||
|
CONFIGURE_SUDO=true
|
||||||
|
DNS_UPDATE=true
|
||||||
|
NTP_SERVER=""
|
||||||
|
ENABLE_FIDO2=false
|
||||||
|
FIDO2_USERS=()
|
||||||
|
UNINSTALL=false
|
||||||
|
|
||||||
|
# ─── Argument parsing ────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
grep '^#' "$0" | grep -v '#!/' | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-d|--domain) IPA_DOMAIN="$2"; shift 2 ;;
|
||||||
|
-r|--realm) IPA_REALM="$2"; shift 2 ;;
|
||||||
|
-s|--server) IPA_SERVER="$2"; shift 2 ;;
|
||||||
|
-h|--hostname) IPA_HOSTNAME="$2"; shift 2 ;;
|
||||||
|
-p|--principal) IPA_PRINCIPAL="$2"; shift 2 ;;
|
||||||
|
-w|--password) IPA_PASSWORD="$2"; shift 2 ;;
|
||||||
|
-m|--mkhomedir) MKHOMEDIR=true; shift ;;
|
||||||
|
--no-mkhomedir) MKHOMEDIR=false; shift ;;
|
||||||
|
--sudo) CONFIGURE_SUDO=true; shift ;;
|
||||||
|
--no-sudo) CONFIGURE_SUDO=false; shift ;;
|
||||||
|
--no-dns-update) DNS_UPDATE=false; shift ;;
|
||||||
|
--ntp-server) NTP_SERVER="$2"; shift 2 ;;
|
||||||
|
--fido2) ENABLE_FIDO2=true; shift ;;
|
||||||
|
--fido2-user) FIDO2_USERS+=("$2"); shift 2 ;;
|
||||||
|
--uninstall) UNINSTALL=true; shift ;;
|
||||||
|
--help) usage ;;
|
||||||
|
*) error "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── Validation ──────────────────────────────────────────────────────────────
|
||||||
|
section "Validating inputs"
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
||||||
|
|
||||||
|
if [[ "$UNINSTALL" == false ]]; then
|
||||||
|
[[ -z "$IPA_DOMAIN" ]] && { error "--domain is required"; exit 1; }
|
||||||
|
[[ -z "$IPA_SERVER" ]] && { error "--server is required"; exit 1; }
|
||||||
|
|
||||||
|
if [[ -z "$IPA_REALM" ]]; then
|
||||||
|
IPA_REALM="${IPA_DOMAIN^^}"
|
||||||
|
log "Realm derived from domain: $IPA_REALM"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$IPA_HOSTNAME" ]]; then
|
||||||
|
IPA_HOSTNAME=$(hostname -f)
|
||||||
|
log "Using current hostname: $IPA_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$IPA_PASSWORD" ]]; then
|
||||||
|
read -rsp "Enter password for ${IPA_PRINCIPAL}@${IPA_REALM}: " IPA_PASSWORD
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "$IPA_PASSWORD" ]] && { error "Password is required"; exit 1; }
|
||||||
|
|
||||||
|
# If --fido2-user was passed, implicitly enable fido2
|
||||||
|
[[ ${#FIDO2_USERS[@]} -gt 0 ]] && ENABLE_FIDO2=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Uninstall path ───────────────────────────────────────────────────────────
|
||||||
|
if [[ "$UNINSTALL" == true ]]; then
|
||||||
|
section "Uninstalling FreeIPA client"
|
||||||
|
if command -v ipa-client-install &>/dev/null; then
|
||||||
|
ipa-client-install --uninstall --unattended
|
||||||
|
log "Client uninstalled"
|
||||||
|
else
|
||||||
|
warn "ipa-client-install not found — may not have been installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up FIDO2 PAM config if present
|
||||||
|
if [[ -f /etc/pam.d/fido2-auth ]]; then
|
||||||
|
rm -f /etc/pam.d/fido2-auth
|
||||||
|
log "Removed FIDO2 PAM config"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Detect OS ───────────────────────────────────────────────────────────────
|
||||||
|
section "Detecting OS"
|
||||||
|
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
source /etc/os-release
|
||||||
|
OS_ID="${ID}"
|
||||||
|
OS_VERSION="${VERSION_ID%%.*}"
|
||||||
|
log "Detected: $PRETTY_NAME"
|
||||||
|
else
|
||||||
|
error "Cannot detect OS — /etc/os-release missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$OS_ID" in
|
||||||
|
rhel|centos|rocky|almalinux|fedora)
|
||||||
|
PKG_MANAGER="dnf"
|
||||||
|
IPA_CLIENT_PKG="freeipa-client"
|
||||||
|
SSSD_PKGS="sssd sssd-ipa sssd-tools"
|
||||||
|
ODDJOB_PKGS="oddjob oddjob-mkhomedir"
|
||||||
|
CHRONY_PKG="chrony"
|
||||||
|
FIDO2_PKGS="pam-u2f"
|
||||||
|
FIDO2_TOOLS_PKGS="pamu2fcfg"
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
PKG_MANAGER="apt-get"
|
||||||
|
IPA_CLIENT_PKG="freeipa-client"
|
||||||
|
SSSD_PKGS="sssd sssd-ipa"
|
||||||
|
ODDJOB_PKGS=""
|
||||||
|
CHRONY_PKG="chrony"
|
||||||
|
FIDO2_PKGS="libpam-u2f"
|
||||||
|
FIDO2_TOOLS_PKGS="pamu2fcfg"
|
||||||
|
warn "Debian/Ubuntu support is best-effort — Rocky/Alma recommended"
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
PKG_MANAGER="pacman"
|
||||||
|
IPA_CLIENT_PKG="freeipa"
|
||||||
|
SSSD_PKGS="sssd"
|
||||||
|
ODDJOB_PKGS="oddjob"
|
||||||
|
CHRONY_PKG="chrony"
|
||||||
|
FIDO2_PKGS="pam-u2f"
|
||||||
|
FIDO2_TOOLS_PKGS="pamu2fcfg"
|
||||||
|
warn "Arch Linux: ensure the AUR helper (yay/paru) is available for AUR packages"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unsupported OS: $OS_ID"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ─── Helper: install packages ─────────────────────────────────────────────────
|
||||||
|
pkg_install() {
|
||||||
|
local pkgs=("$@")
|
||||||
|
[[ ${#pkgs[@]} -eq 0 ]] && return 0
|
||||||
|
case "$PKG_MANAGER" in
|
||||||
|
dnf) dnf install -y "${pkgs[@]}" ;;
|
||||||
|
apt-get) DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkgs[@]}" ;;
|
||||||
|
pacman) pacman -Sy --noconfirm "${pkgs[@]}" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Pre-flight checks ────────────────────────────────────────────────────────
|
||||||
|
section "Pre-flight checks"
|
||||||
|
|
||||||
|
log "Checking connectivity to IPA server $IPA_SERVER..."
|
||||||
|
if ! ping -c 2 -W 3 "$IPA_SERVER" &>/dev/null; then
|
||||||
|
error "Cannot reach IPA server $IPA_SERVER — check network/DNS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for port in 443 88; do
|
||||||
|
if ! timeout 5 bash -c "echo >/dev/tcp/$IPA_SERVER/$port" 2>/dev/null; then
|
||||||
|
warn "Port $port on $IPA_SERVER unreachable — enrollment may fail"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# FIDO2 pre-flight: check a key is actually plugged in
|
||||||
|
if [[ "$ENABLE_FIDO2" == true ]]; then
|
||||||
|
if ! command -v fido2-token &>/dev/null; then
|
||||||
|
warn "fido2-token not found yet — will install libfido2 tooling"
|
||||||
|
else
|
||||||
|
FIDO2_DEVICES=$(fido2-token -L 2>/dev/null | wc -l)
|
||||||
|
if [[ "$FIDO2_DEVICES" -eq 0 ]]; then
|
||||||
|
warn "No FIDO2 devices detected. Plug in the key before the registration step."
|
||||||
|
else
|
||||||
|
log "Detected $FIDO2_DEVICES FIDO2 device(s)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Checking time synchronization..."
|
||||||
|
if command -v chronyc &>/dev/null; then
|
||||||
|
OFFSET=$(chronyc tracking 2>/dev/null | awk '/System time/ {print $4}' | tr -d '-')
|
||||||
|
if [[ -n "$OFFSET" ]]; then
|
||||||
|
INT_OFFSET=${OFFSET%.*}
|
||||||
|
if [[ "${INT_OFFSET:-0}" -gt 60 ]]; then
|
||||||
|
warn "Time offset is ${OFFSET}s — Kerberos requires <300s skew"
|
||||||
|
else
|
||||||
|
log "Time offset: ${OFFSET}s — OK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "chrony not found — installing and syncing"
|
||||||
|
pkg_install "$CHRONY_PKG"
|
||||||
|
systemctl enable --now chronyd
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$NTP_SERVER" ]]; then
|
||||||
|
log "Configuring NTP server: $NTP_SERVER"
|
||||||
|
echo "server $NTP_SERVER iburst" >> /etc/chrony.conf
|
||||||
|
systemctl restart chronyd
|
||||||
|
chronyc makestep
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f /etc/ipa/default.conf ]]; then
|
||||||
|
warn "This host appears to already be enrolled in an IPA domain"
|
||||||
|
read -rp "Re-enroll? This will uninstall first. [y/N]: " CONFIRM
|
||||||
|
if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
|
||||||
|
log "Uninstalling existing client..."
|
||||||
|
ipa-client-install --uninstall --unattended || true
|
||||||
|
else
|
||||||
|
log "Aborting"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Install packages ─────────────────────────────────────────────────────────
|
||||||
|
section "Installing packages"
|
||||||
|
|
||||||
|
# On Arch, freeipa pulls ipa-client; sssd is separate
|
||||||
|
if [[ "$OS_ID" == "arch" ]]; then
|
||||||
|
# Ensure pacman db is fresh
|
||||||
|
pacman -Sy --noconfirm
|
||||||
|
pkg_install $IPA_CLIENT_PKG $SSSD_PKGS $ODDJOB_PKGS
|
||||||
|
|
||||||
|
# Arch: pam-u2f may be in AUR — try pacman first, fall back to yay/paru
|
||||||
|
if [[ "$ENABLE_FIDO2" == true ]]; then
|
||||||
|
if ! pacman -Qi pam-u2f &>/dev/null; then
|
||||||
|
if command -v yay &>/dev/null; then
|
||||||
|
log "Installing pam-u2f via yay (AUR)"
|
||||||
|
sudo -u nobody yay -S --noconfirm pam-u2f pamu2fcfg 2>/dev/null || \
|
||||||
|
warn "AUR install failed — install pam-u2f manually from AUR"
|
||||||
|
elif command -v paru &>/dev/null; then
|
||||||
|
log "Installing pam-u2f via paru (AUR)"
|
||||||
|
sudo -u nobody paru -S --noconfirm pam-u2f pamu2fcfg 2>/dev/null || \
|
||||||
|
warn "AUR install failed — install pam-u2f manually from AUR"
|
||||||
|
else
|
||||||
|
warn "pam-u2f not in official repos and no AUR helper found."
|
||||||
|
warn "Install manually: yay -S pam-u2f pamu2fcfg"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# libfido2 for fido2-token tooling
|
||||||
|
pkg_install libfido2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
pkg_install $IPA_CLIENT_PKG $SSSD_PKGS
|
||||||
|
|
||||||
|
if [[ -n "$ODDJOB_PKGS" ]]; then
|
||||||
|
pkg_install $ODDJOB_PKGS
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ENABLE_FIDO2" == true ]]; then
|
||||||
|
pkg_install $FIDO2_PKGS libfido2
|
||||||
|
# pamu2fcfg may be a separate package on some distros
|
||||||
|
pkg_install $FIDO2_TOOLS_PKGS 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Packages installed"
|
||||||
|
|
||||||
|
# ─── Set hostname ─────────────────────────────────────────────────────────────
|
||||||
|
section "Configuring hostname"
|
||||||
|
|
||||||
|
CURRENT_HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||||||
|
if [[ "$CURRENT_HOSTNAME" != "$IPA_HOSTNAME" ]]; then
|
||||||
|
log "Setting hostname to $IPA_HOSTNAME"
|
||||||
|
hostnamectl set-hostname "$IPA_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOST_IP=$(ip route get 1 | awk '{print $7; exit}')
|
||||||
|
SHORT_NAME="${IPA_HOSTNAME%%.*}"
|
||||||
|
|
||||||
|
sed -i "/\b${IPA_HOSTNAME}\b/d" /etc/hosts
|
||||||
|
sed -i "/\b${SHORT_NAME}\b/d" /etc/hosts
|
||||||
|
echo "$HOST_IP $IPA_HOSTNAME $SHORT_NAME" >> /etc/hosts
|
||||||
|
log "Updated /etc/hosts: $HOST_IP $IPA_HOSTNAME"
|
||||||
|
|
||||||
|
# ─── Arch-specific pre-enrollment setup ──────────────────────────────────────
|
||||||
|
if [[ "$OS_ID" == "arch" ]]; then
|
||||||
|
section "Arch-specific setup"
|
||||||
|
|
||||||
|
# Arch ships nscd rather than sssd providing nss by default;
|
||||||
|
# make sure nsswitch.conf will work with sss
|
||||||
|
if ! grep -q "sss" /etc/nsswitch.conf; then
|
||||||
|
sed -i 's/^passwd:.*/passwd: files sss/' /etc/nsswitch.conf
|
||||||
|
sed -i 's/^group:.*/group: files sss/' /etc/nsswitch.conf
|
||||||
|
sed -i 's/^shadow:.*/shadow: files sss/' /etc/nsswitch.conf
|
||||||
|
log "Updated nsswitch.conf for SSSD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure the IPA CA cert directory exists (ipa-client-install expects it)
|
||||||
|
mkdir -p /etc/ipa
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Run ipa-client-install ───────────────────────────────────────────────────
|
||||||
|
section "Enrolling with FreeIPA"
|
||||||
|
|
||||||
|
ENROLL_ARGS=(
|
||||||
|
--domain="$IPA_DOMAIN"
|
||||||
|
--realm="$IPA_REALM"
|
||||||
|
--server="$IPA_SERVER"
|
||||||
|
--hostname="$IPA_HOSTNAME"
|
||||||
|
--principal="$IPA_PRINCIPAL"
|
||||||
|
--password="$IPA_PASSWORD"
|
||||||
|
--unattended
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ "$MKHOMEDIR" == true ]] && ENROLL_ARGS+=(--mkhomedir)
|
||||||
|
[[ "$DNS_UPDATE" == false ]] && ENROLL_ARGS+=(--no-dns-update)
|
||||||
|
[[ "$CONFIGURE_SUDO" == true ]] && ENROLL_ARGS+=(--enable-dns-updates)
|
||||||
|
|
||||||
|
log "Running ipa-client-install..."
|
||||||
|
if ipa-client-install "${ENROLL_ARGS[@]}"; then
|
||||||
|
log "Enrollment successful"
|
||||||
|
else
|
||||||
|
error "ipa-client-install failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── SSSD configuration ───────────────────────────────────────────────────────
|
||||||
|
section "Configuring SSSD"
|
||||||
|
|
||||||
|
if [[ "$CONFIGURE_SUDO" == true ]]; then
|
||||||
|
if grep -q "^services" /etc/sssd/sssd.conf; then
|
||||||
|
if ! grep -q "sudo" /etc/sssd/sssd.conf; then
|
||||||
|
sed -i 's/^services = .*/& , sudo/' /etc/sssd/sssd.conf
|
||||||
|
log "Added sudo to SSSD services"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "^sudoers:.*sss" /etc/nsswitch.conf; then
|
||||||
|
if grep -q "^sudoers:" /etc/nsswitch.conf; then
|
||||||
|
sed -i 's/^sudoers:.*/sudoers: files sss/' /etc/nsswitch.conf
|
||||||
|
else
|
||||||
|
echo "sudoers: files sss" >> /etc/nsswitch.conf
|
||||||
|
fi
|
||||||
|
log "Configured nsswitch.conf for sudo via SSSD"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl restart sssd
|
||||||
|
systemctl enable sssd
|
||||||
|
log "SSSD restarted"
|
||||||
|
|
||||||
|
# ─── PAM / homedir ────────────────────────────────────────────────────────────
|
||||||
|
section "Configuring PAM"
|
||||||
|
|
||||||
|
if [[ "$MKHOMEDIR" == true ]]; then
|
||||||
|
case "$OS_ID" in
|
||||||
|
rhel|centos|rocky|almalinux|fedora)
|
||||||
|
systemctl enable --now oddjobd 2>/dev/null || true
|
||||||
|
authselect enable-feature with-mkhomedir 2>/dev/null || \
|
||||||
|
authconfig --enablemkhomedir --update 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
# Arch uses pam_mkhomedir directly in PAM stack
|
||||||
|
if ! grep -q "pam_mkhomedir" /etc/pam.d/system-login; then
|
||||||
|
sed -i '/^session.*pam_systemd/a session optional pam_mkhomedir.so skel=/etc/skel umask=077' \
|
||||||
|
/etc/pam.d/system-login
|
||||||
|
log "Added pam_mkhomedir to /etc/pam.d/system-login"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
pam-auth-update --enable mkhomedir 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
log "Home directory auto-creation enabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── FIDO2 configuration ──────────────────────────────────────────────────────
|
||||||
|
if [[ "$ENABLE_FIDO2" == true ]]; then
|
||||||
|
section "Configuring FIDO2 authentication"
|
||||||
|
|
||||||
|
# Check pam_u2f.so is actually present after install
|
||||||
|
PAM_U2F_SO=$(find /usr/lib /lib -name "pam_u2f.so" 2>/dev/null | head -1)
|
||||||
|
if [[ -z "$PAM_U2F_SO" ]]; then
|
||||||
|
error "pam_u2f.so not found — FIDO2 PAM module not installed correctly"
|
||||||
|
warn "Skipping FIDO2 configuration. Install pam-u2f manually and re-run with --fido2"
|
||||||
|
else
|
||||||
|
log "Found pam_u2f.so at: $PAM_U2F_SO"
|
||||||
|
|
||||||
|
# ── Central credential store ─────────────────────────────────────────
|
||||||
|
# We use a central /etc/u2f_mappings file so credentials are stored
|
||||||
|
# system-wide (not per-user ~/.config/Yubico/u2f_keys), which is
|
||||||
|
# easier to manage and distribute via Ansible/Puppet.
|
||||||
|
U2F_MAPPINGS="/etc/u2f_mappings"
|
||||||
|
touch "$U2F_MAPPINGS"
|
||||||
|
chmod 600 "$U2F_MAPPINGS"
|
||||||
|
|
||||||
|
# ── PAM stack integration ─────────────────────────────────────────────
|
||||||
|
# Strategy: FIDO2 as a SECOND factor (after password) for sudo and sshd.
|
||||||
|
# Change 'required' to 'sufficient' if you want FIDO2 as sole factor.
|
||||||
|
#
|
||||||
|
# We write a drop-in snippet and include it rather than patching
|
||||||
|
# distro PAM files directly, to survive pam-config updates.
|
||||||
|
|
||||||
|
PAM_FIDO2_SNIPPET="/etc/pam.d/fido2-u2f"
|
||||||
|
cat > "$PAM_FIDO2_SNIPPET" <<'PAMEOF'
|
||||||
|
# pam-u2f FIDO2 second factor
|
||||||
|
# auth required = must present FIDO2 key (touch required by default)
|
||||||
|
# nouserok = allow login even if user has no key registered
|
||||||
|
# (remove this once all users have keys enrolled)
|
||||||
|
# authfile = path to central credential mapping file
|
||||||
|
auth required pam_u2f.so authfile=/etc/u2f_mappings nouserok cue
|
||||||
|
PAMEOF
|
||||||
|
|
||||||
|
# Inject into sudo PAM — after the first 'auth' line (after password)
|
||||||
|
configure_pam_sudo() {
|
||||||
|
local pam_file="$1"
|
||||||
|
if [[ -f "$pam_file" ]] && ! grep -q "pam_u2f" "$pam_file"; then
|
||||||
|
# Insert after the last 'auth' line that isn't pam_u2f
|
||||||
|
sed -i '/^auth.*pam_sss\|^auth.*pam_unix/a auth include fido2-u2f' "$pam_file"
|
||||||
|
log "Injected FIDO2 into $pam_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$OS_ID" in
|
||||||
|
rhel|centos|rocky|almalinux|fedora)
|
||||||
|
configure_pam_sudo /etc/pam.d/sudo
|
||||||
|
configure_pam_sudo /etc/pam.d/sshd
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
configure_pam_sudo /etc/pam.d/sudo
|
||||||
|
configure_pam_sudo /etc/pam.d/sshd
|
||||||
|
;;
|
||||||
|
debian|ubuntu)
|
||||||
|
configure_pam_sudo /etc/pam.d/sudo
|
||||||
|
configure_pam_sudo /etc/pam.d/sshd
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── udev rule for FIDO2 devices ───────────────────────────────────────
|
||||||
|
# Without this, non-root users can't talk to the USB HID device
|
||||||
|
UDEV_RULE="/etc/udev/rules.d/70-u2f.rules"
|
||||||
|
if [[ ! -f "$UDEV_RULE" ]]; then
|
||||||
|
cat > "$UDEV_RULE" <<'UDEVEOF'
|
||||||
|
# FIDO2/U2F device access for all users
|
||||||
|
# Covers YubiKey, Nitrokey, SoloKey, Google Titan, and generic FIDO2 keys
|
||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1050", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="096e", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="20a0", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1d6b", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="2581", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="2c97", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
|
UDEVEOF
|
||||||
|
udevadm control --reload-rules
|
||||||
|
udevadm trigger
|
||||||
|
log "Installed udev rules for FIDO2 devices"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── plugdev group ─────────────────────────────────────────────────────
|
||||||
|
if ! getent group plugdev &>/dev/null; then
|
||||||
|
groupadd plugdev
|
||||||
|
log "Created plugdev group"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Register keys for specified users ─────────────────────────────────
|
||||||
|
if [[ ${#FIDO2_USERS[@]} -gt 0 ]]; then
|
||||||
|
section "Registering FIDO2 keys"
|
||||||
|
info "You will be prompted to touch your FIDO2 key for each user."
|
||||||
|
info "Credentials are stored in $U2F_MAPPINGS"
|
||||||
|
echo
|
||||||
|
|
||||||
|
for IPA_USER in "${FIDO2_USERS[@]}"; do
|
||||||
|
echo -e "${CYAN}━━━ Registering key for user: $IPA_USER ━━━${NC}"
|
||||||
|
|
||||||
|
# Check if user already has an entry
|
||||||
|
if grep -q "^${IPA_USER}:" "$U2F_MAPPINGS" 2>/dev/null; then
|
||||||
|
warn "User $IPA_USER already has a FIDO2 credential in $U2F_MAPPINGS"
|
||||||
|
read -rp " Add an additional key for $IPA_USER? [y/N]: " ADD_MORE
|
||||||
|
[[ ! "$ADD_MORE" =~ ^[Yy]$ ]] && continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Insert the FIDO2 key for $IPA_USER and press Enter when ready..."
|
||||||
|
read -r
|
||||||
|
|
||||||
|
# Check key is present
|
||||||
|
if command -v fido2-token &>/dev/null; then
|
||||||
|
DETECTED=$(fido2-token -L 2>/dev/null | wc -l)
|
||||||
|
if [[ "$DETECTED" -eq 0 ]]; then
|
||||||
|
warn "No FIDO2 device detected — skipping $IPA_USER"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Touch the key when it blinks..."
|
||||||
|
|
||||||
|
# pamu2fcfg outputs: username:credentialID,publicKey
|
||||||
|
if NEW_CRED=$(pamu2fcfg -u "$IPA_USER" -o "pam://$(hostname -f)" 2>/dev/null); then
|
||||||
|
if grep -q "^${IPA_USER}:" "$U2F_MAPPINGS"; then
|
||||||
|
# Append additional key to existing line (pamu2fcfg format)
|
||||||
|
EXTRA=$(echo "$NEW_CRED" | cut -d: -f2-)
|
||||||
|
sed -i "s|^${IPA_USER}:.*|&:${EXTRA}|" "$U2F_MAPPINGS"
|
||||||
|
log "Added additional FIDO2 key for $IPA_USER"
|
||||||
|
else
|
||||||
|
echo "$NEW_CRED" >> "$U2F_MAPPINGS"
|
||||||
|
log "Registered FIDO2 key for $IPA_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add user to plugdev group
|
||||||
|
usermod -aG plugdev "$IPA_USER" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
error "pamu2fcfg failed for $IPA_USER — key may not support this operation"
|
||||||
|
warn "You can register manually later:"
|
||||||
|
warn " pamu2fcfg -u $IPA_USER -o pam://\$(hostname -f) >> $U2F_MAPPINGS"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# No users specified — print instructions for manual registration
|
||||||
|
info "No --fido2-user specified. To register keys manually:"
|
||||||
|
echo
|
||||||
|
echo " # As root, for each user:"
|
||||||
|
echo " pamu2fcfg -u <username> -o pam://\$(hostname -f) >> $U2F_MAPPINGS"
|
||||||
|
echo
|
||||||
|
echo " # Then add user to plugdev group:"
|
||||||
|
echo " usermod -aG plugdev <username>"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "FIDO2 PAM configuration complete"
|
||||||
|
info "Note: 'nouserok' is set — users without a registered key can still log in."
|
||||||
|
info "Remove 'nouserok' from $PAM_FIDO2_SNIPPET once all users have keys enrolled."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── SSH configuration ────────────────────────────────────────────────────────
|
||||||
|
section "Configuring SSH"
|
||||||
|
|
||||||
|
SSHD_CONFIG="/etc/ssh/sshd_config"
|
||||||
|
|
||||||
|
set_sshd_option() {
|
||||||
|
local key="$1" val="$2"
|
||||||
|
if grep -qE "^#?${key}" "$SSHD_CONFIG"; then
|
||||||
|
sed -i "s|^#\?${key}.*|${key} ${val}|" "$SSHD_CONFIG"
|
||||||
|
else
|
||||||
|
echo "${key} ${val}" >> "$SSHD_CONFIG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
set_sshd_option "GSSAPIAuthentication" "yes"
|
||||||
|
set_sshd_option "GSSAPICleanupCredentials" "yes"
|
||||||
|
set_sshd_option "AuthorizedKeysCommand" "/usr/bin/sss_ssh_authorizedkeys"
|
||||||
|
set_sshd_option "AuthorizedKeysCommandUser" "nobody"
|
||||||
|
|
||||||
|
# If FIDO2 is enabled, allow keyboard-interactive so pam_u2f can prompt for touch
|
||||||
|
if [[ "$ENABLE_FIDO2" == true ]]; then
|
||||||
|
set_sshd_option "ChallengeResponseAuthentication" "yes"
|
||||||
|
set_sshd_option "AuthenticationMethods" "publickey,keyboard-interactive"
|
||||||
|
log "SSH configured for FIDO2 second factor (keyboard-interactive)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl restart sshd
|
||||||
|
log "SSH configured"
|
||||||
|
|
||||||
|
# ─── Verify enrollment ────────────────────────────────────────────────────────
|
||||||
|
section "Verifying enrollment"
|
||||||
|
|
||||||
|
if echo "$IPA_PASSWORD" | kinit "${IPA_PRINCIPAL}@${IPA_REALM}" &>/dev/null; then
|
||||||
|
log "Kerberos authentication: OK"
|
||||||
|
kdestroy &>/dev/null
|
||||||
|
else
|
||||||
|
warn "Could not obtain Kerberos ticket — check credentials/connectivity"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if id "admin" &>/dev/null; then
|
||||||
|
log "SSSD user lookup: OK (resolved 'admin' from IPA)"
|
||||||
|
else
|
||||||
|
warn "SSSD user lookup failed — SSSD may still be starting up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Summary ──────────────────────────────────────────────────────────────────
|
||||||
|
section "Enrollment complete"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Host: $IPA_HOSTNAME
|
||||||
|
Domain: $IPA_DOMAIN
|
||||||
|
Realm: $IPA_REALM
|
||||||
|
Server: $IPA_SERVER
|
||||||
|
OS: $PRETTY_NAME
|
||||||
|
Homedir: $MKHOMEDIR
|
||||||
|
Sudo: $CONFIGURE_SUDO
|
||||||
|
FIDO2: $ENABLE_FIDO2
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ "$ENABLE_FIDO2" == true ]] && [[ ${#FIDO2_USERS[@]} -gt 0 ]]; then
|
||||||
|
echo " FIDO2 users registered: ${FIDO2_USERS[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
• Test login: ssh <ipa-user>@$(hostname -f)
|
||||||
|
• Check SSSD: sssctl domain-status <domain>
|
||||||
|
• Check Kerberos: kinit <user>@<REALM>
|
||||||
|
• List FIDO2 creds: cat /etc/u2f_mappings
|
||||||
|
• Register key: pamu2fcfg -u <user> -o pam://$(hostname -f) >> /etc/u2f_mappings
|
||||||
|
|
||||||
|
EOF
|
||||||
Loading…
Reference in New Issue