6 Commits
Author SHA1 Message Date
beatzaplentyandClaude Sonnet 4.6 afabcbbeff feat(raspberrypi): add Raspberry Pi configuration section
Secret Scan / Scan for secrets and sensitive config (push) Failing after 5s
Documents and scripts to reproduce the IPA integration on the Pi
(raspberrypi.tail13f623.ts.net, Debian 12 bookworm):

- setup-ipa-sudo.sh: writes /etc/sudoers.d/ipa-admins granting
  %admins NOPASSWD:ALL (same IPA admins group as pbs/pdm/pve1)
- setup-docker-ipa-gid.sh: pins local docker group GID to 50010
  via groupmod --non-unique so IPA docker-access group membership
  alone grants docker socket access (mirrors NixOS lib.mkForce approach)
- README.md + CLAUDE.md: quick-start, current status, guardrails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 15:47:30 +10:00
beatzaplenty a4d907e2eb Merge pull request 'docs(freeipa): update IPs and remove Pi-hole references' (#5) from worktree-debian-full-sweep into main
Secret Scan / Scan for secrets and sensitive config (push) Failing after 3s
Reviewed-on: #5
2026-07-28 05:26:40 +00:00
beatzaplentyandClaude Sonnet 4.6 761fe9e194 docs(freeipa): update IPs and remove Pi-hole references
Secret Scan / Scan for secrets and sensitive config (push) Failing after 4s
Secret Scan / Scan for secrets and sensitive config (pull_request) Failing after 3s
- IPA now runs at 192.168.2.253 (previously shown as .138 in install docs)
- cloud-init nameserver and nmcli DNS during install use .254 (gateway)
  since IPA isn't running yet at that point
- --forwarder updated to 192.168.2.254 (LAN gateway, Pi-hole is gone)
- Remove Step 7 (Pi-hole DNS forwarding config) from install.md
- Delete obsolete freeipa/docs/pihole-dns.md and configure-pihole-dns.sh
- Update CLAUDE.md section count and freeipa/ guidance entry
- Update freeipa/README.md IP and DNS forwarding description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 15:25:22 +10:00
beatzaplentyandClaude Sonnet 4.6 0c15de3329 proxmox: add IPA sudo and local backdoor scripts, update bootstrap notes
Secret Scan / Scan for secrets and sensitive config (push) Failing after 3s
- setup-ipa-sudo.sh: grants %admins group NOPASSWD sudo after ipa-client-install;
  writes admins-proxmox (pvesh/qm/pct) only when those binaries are present,
  so the same script works on PBS/PDM as well as PVE hosts
- create-local-backdoor.sh: creates a local 'pveadmin' account with SSH key
  and NOPASSWD sudo as an emergency fallback when IPA/SSSD is unavailable;
  password set via BACKDOOR_PASS env var or prompted interactively
- bootstrap.sh: appended post-IPA-enrollment reminder to the final checklist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 13:32:30 +10:00
beatzaplenty f54e00ebc8 Merge pull request 'feat(freeipa): add domain-controller setup — Rocky Linux 9 + FreeIPA 4.13' (#5) from feat/freeipa-domain-controller into main
Secret Scan / Scan for secrets and sensitive config (push) Failing after 4s
Reviewed-on: #5
2026-07-26 21:38:46 +00:00
beatzaplentyandClaude Sonnet 4.6 89cc8c8cc3 feat(freeipa): add domain-controller setup — Rocky Linux 9 + FreeIPA 4.13
Installs and documents the FreeIPA identity management server at
domain-controller.sweet.home (VMID 108, pve1). Provides Kerberos,
LDAP, and integrated DNS for the SWEET.HOME realm.

New section: freeipa/
- docs/install.md: full step-by-step reproduction procedure including
  Proxmox VM prep (Rocky Linux 9 GenericCloud, SeaBIOS, cloud-init),
  swap setup, static IP, /etc/hosts fix, ipa-server-install flags
- docs/pihole-dns.md: how to configure Pi-hole to forward sweet.home
  queries to the FreeIPA BIND instance
- scripts/install.sh: idempotent install script with pre-flight checks;
  reads passwords from env or interactive prompt (never commits them)
- scripts/configure-pihole-dns.sh: idempotent Pi-hole forwarder setup
- scripts/verify.sh: read-only health check (13 checks, 0 side effects)
- CLAUDE.md: host guardrails for domain-controller

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Jbvxx4xbHVcx1NkK3vtmK
2026-07-27 07:22:05 +10:00
15 changed files with 928 additions and 5 deletions

No files matched your search

+11 -2
View File
@@ -6,8 +6,9 @@ OVERRIDE any default behaviour and must be followed exactly.
## Repo purpose ## Repo purpose
Configuration toolsets and docs for Wayne's Debian-based LAN machines. Configuration toolsets and docs for Wayne's Debian-based LAN machines.
Two sections currently exist: `proxmox/` (Proxmox VE hosts) and Four sections currently exist: `proxmox/` (Proxmox VE hosts),
`pihole/` (Pi-hole DNS/DHCP). Each has its own README. `pihole/` (Pi-hole DNS/DHCP), `freeipa/` (FreeIPA identity management),
and `raspberrypi/` (Raspberry Pi 4). Each has its own README.
## Safety rules (apply everywhere in this repo) ## Safety rules (apply everywhere in this repo)
@@ -35,6 +36,14 @@ Two sections currently exist: `proxmox/` (Proxmox VE hosts) and
rules above. `apply-config.sh` is the only script that touches a live rules above. `apply-config.sh` is the only script that touches a live
host; treat its `<dest-host>` argument as production unless you're host; treat its `<dest-host>` argument as production unless you're
explicitly testing on a throwaway instance. explicitly testing on a throwaway instance.
- **FreeIPA:** `freeipa/` documents and scripts for the FreeIPA identity
management server (`domain-controller.sweet.home`, VMID 108 on `pve1`).
All scripts that SSH into the server are production operations — treat
them as off-limits without an explicit same-session go-ahead.
- **Raspberry Pi:** see `raspberrypi/CLAUDE.md` — production host running
live services; same write-authorisation rules as `pve1`. Bootstrap access
via local `raspi` user (NOPASSWD sudo). IPA-enrolled; docker GID pinned
to 50010 to match IPA `docker-access` group.
## Adding a new machine type ## Adding a new machine type
+20 -3
View File
@@ -7,9 +7,11 @@ service type.
## Layout ## Layout
``` ```
proxmox/ Proxmox VE hosts (pve1 production, pve-test sandbox) proxmox/ Proxmox VE hosts (pve1 production, pve-test sandbox)
pihole/ Pi-hole DNS/DHCP (config snapshots, pull/apply scripts) pihole/ Pi-hole DNS/DHCP (config snapshots, pull/apply scripts)
scripts/ Repo-wide scripts (secret scanning, git hook installer) freeipa/ FreeIPA identity management server (domain-controller.sweet.home)
raspberrypi/ Raspberry Pi 4 (Debian 12, IPA-enrolled, Docker host)
scripts/ Repo-wide scripts (secret scanning, git hook installer)
``` ```
## Sections ## Sections
@@ -25,6 +27,21 @@ Pi-hole v6 configuration management. Stores a sanitised snapshot of the
live config and provides scripts to pull from or push to a running live config and provides scripts to pull from or push to a running
instance. See `pihole/README.md` for usage. instance. See `pihole/README.md` for usage.
### `freeipa/`
FreeIPA 4.x identity management server running on Rocky Linux 9
(`domain-controller.sweet.home`, VMID 108 on pve1). Provides Kerberos,
LDAP, and integrated DNS for the `sweet.home` realm. See
`freeipa/README.md` for the quick-start and `freeipa/docs/install.md`
for the full reproduction procedure.
### `raspberrypi/`
Raspberry Pi 4 running Debian 12 bookworm (`raspberrypi.tail13f623.ts.net`,
reachable from LAN via Tailscale MagicDNS). IPA-enrolled; runs Traefik,
Uptime Kuma, CrowdSec, and Beszel. See `raspberrypi/README.md` for
setup scripts and current status.
## Secret scanning ## Secret scanning
All commits are scanned for secrets by a CI pipeline that runs on both All commits are scanned for secrets by a CI pipeline that runs on both
+33
View File
@@ -0,0 +1,33 @@
# freeipa/CLAUDE.md
Host-specific guardrails for the `domain-controller` FreeIPA server.
## Host: `domain-controller.sweet.home`
- **VMID 108 on `pve1.sweet.home`** — this is production infrastructure.
Treat it the same as any other pve1 guest: no changes without explicit
same-session operator authorisation.
- **Read-only is always fine**: SSH in as `wayne`, inspect IPA state with
`ipa *` commands or `kinit admin && ipa ...`, check service status with
`ipactl status` — none of that needs authorisation.
- **Never modify FreeIPA topology, replicas, or the LDAP DIT directly**
without the operator's go-ahead. That means no `ipa user-del`, no
`ipa-replica-manage`, no `ldapmodify` against the live directory.
- **Do not commit secrets.** The Directory Manager password and the `admin`
Kerberos password must not appear in any file in this repo. Scripts that
need them must read from environment variables or prompt interactively.
- The `admin` password and Directory Manager password were generated at
install time and stored only in the operator's password manager — not in
this repo. See `README.md` for how to retrieve/reset them.
## What is safe to run automatically
- `scripts/verify.sh` — read-only health check, no side effects.
- `scripts/configure-pihole-dns.sh` — idempotent DNS forwarder setup in
Pi-hole; safe to re-run.
## What requires operator go-ahead
- `scripts/install.sh` — destructive if run against an already-provisioned
host. Always check first with `ipactl status`.
- Any `ipa-replica-install` or `ipa-server-upgrade` invocation.
+88
View File
@@ -0,0 +1,88 @@
# FreeIPA — domain-controller.sweet.home
FreeIPA 4.x identity management server providing Kerberos, LDAP, and
integrated DNS for the `sweet.home` LAN. Runs on Rocky Linux 9 in a
Proxmox VM (VMID 108 on `pve1.sweet.home`).
## Quick status
| Item | Value |
|------|-------|
| Host | `domain-controller.sweet.home` |
| IP | `192.168.2.253` (static) |
| Realm | `SWEET.HOME` |
| Domain | `sweet.home` |
| IPA version | 4.13.x (Rocky Linux 9) |
| Web UI | `https://domain-controller.sweet.home/ipa/ui/` |
| VMID | 108 on `pve1.sweet.home` |
| OS | Rocky Linux 9 (GenericCloud image) |
## What it provides
- **Kerberos KDC** — SSO tickets for the `SWEET.HOME` realm
- **LDAP directory** — centralised user/group/host store (389-ds)
- **Integrated DNS** — authoritative for `sweet.home` and primary LAN
resolver for all hosts, forwarding everything else to the LAN gateway
- **CA** — self-signed CA issuing certs for IPA services
- **Web UI** — at `https://domain-controller.sweet.home/ipa/ui/`
## First-time use
```bash
# SSH to the server
ssh wayne@domain-controller
# Get a Kerberos ticket as admin
kinit admin
# List IPA users
ipa user-find
# Add a user
ipa user-add jdoe --first=John --last=Doe --password
# Check service health
ipactl status
```
## Credentials
The `admin` Kerberos password and Directory Manager password were
generated at install time. They are **not stored in this repo** — keep
them in your password manager.
- **admin** — used for day-to-day IPA management (`kinit admin`)
- **Directory Manager** — low-level LDAP root, rarely needed
To reset the admin password (requires being logged in as admin):
```bash
kinit admin
ipa passwd admin
```
## Ports required (firewalld)
FreeIPA's firewalld config is applied by `ipa-server-install` automatically.
The following ports must be reachable from LAN clients:
| Port | Proto | Service |
|------|-------|---------|
| 80 | TCP | HTTP (redirect to HTTPS) |
| 443 | TCP | HTTPS / Web UI |
| 389 | TCP | LDAP |
| 636 | TCP | LDAPS |
| 88 | TCP+UDP | Kerberos |
| 464 | TCP+UDP | Kerberos password change |
| 53 | TCP+UDP | DNS |
## Reproducing this setup
See `docs/install.md` for the full step-by-step install procedure,
or run `scripts/install.sh` on a fresh Rocky Linux 9 VM with the
correct hostname and IP already set.
## Backup
The CA certificates (required for replicas) are at `/root/cacert.p12`
on the server, encrypted with the Directory Manager password.
Back these up to a secure location.
+242
View File
@@ -0,0 +1,242 @@
# FreeIPA Install Procedure
Full reproduction guide for `domain-controller.sweet.home`. Tested on
Rocky Linux 9.8 (GenericCloud), VMID 108, `pve1.sweet.home`.
## Prerequisites
- Proxmox node with `local-zfs` storage and internet access from guests
- Rocky Linux 9 GenericCloud image downloaded (see step 1)
- SSH access to Proxmox node as a user with `sudo` for `qm`/`pvesm`
- The operator's SSH public key available to inject via cloud-init
---
## Step 1 — Download Rocky Linux 9 GenericCloud image
On the Proxmox node, download to your ISO/image store:
```bash
wget -O /mnt/pve/server-iso/Rocky-9-GenericCloud.latest.x86_64.qcow2 \
https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2
```
The image is ~617 MB. The Proxmox storage must be configured to accept
both ISO images and disk images (set "Content" to include "Disk image"
in the Proxmox UI for that storage).
---
## Step 2 — Prepare the VM in Proxmox
Use an existing VM or create a new one. The config used for VMID 108:
- **CPU**: 2 cores, x86-64-v2-AES
- **RAM**: 2048 MB
- **Disk**: 32 GB on `local-zfs`
- **BIOS**: SeaBIOS (GenericCloud uses MBR — not UEFI)
- **Network**: virtio on `vmbr0`, firewall enabled
- **QEMU guest agent**: enabled
If rebuilding an existing VM (e.g. replacing a prior OS):
```bash
# On pve1 — stop the VM
sudo qm stop <VMID>
# Remove existing disks from config
sudo qm set <VMID> --delete scsi0,efidisk0
# Switch to SeaBIOS if the VM was UEFI
sudo qm set <VMID> --bios seabios
# Free old disk volumes from storage
sudo pvesm free local-zfs:vm-<VMID>-disk-0
sudo pvesm free local-zfs:vm-<VMID>-disk-1
```
### Import the Rocky image and configure cloud-init
```bash
# Import image as a new disk
sudo qm importdisk <VMID> /mnt/pve/server-iso/Rocky-9-GenericCloud.latest.x86_64.qcow2 local-zfs
# Check what disk name was assigned
sudo qm config <VMID> # look for unused0: local-zfs:vm-<VMID>-disk-N
# Attach as scsi0 (adjust disk name from above)
sudo qm set <VMID> --scsi0 local-zfs:vm-<VMID>-disk-0,iothread=1
# Resize to 32 GB
sudo qm disk resize <VMID> scsi0 32G
# Add cloud-init drive
sudo qm set <VMID> --ide2 local-zfs:cloudinit
# Write SSH public key to a temp file
echo 'ssh-rsa AAAA... wayne@stream' > /tmp/admin-key.pub
# (use the key from variables.nix adminSshKey)
# Configure cloud-init
# Use the LAN gateway as temporary DNS — IPA itself will be the DNS at
# 192.168.2.253, but it's not running yet at this point in the install.
sudo qm set <VMID> \
--ciuser wayne \
--sshkeys /tmp/admin-key.pub \
--ipconfig0 ip=dhcp \
--nameserver 192.168.2.254 \
--searchdomain sweet.home
# Set boot order
sudo qm set <VMID> --boot order=scsi0
# Start VM
sudo qm start <VMID>
```
### Note on SSH key mismatch
The GenericCloud image injects the cloud-init SSH key on first boot.
If you need to add an additional key (e.g. from a different machine)
after first boot, mount the disk via nbd while the VM is stopped:
```bash
sudo qm stop <VMID>
sudo qemu-nbd --connect=/dev/nbd1 --format=raw /dev/zvol/rpool/data/vm-<VMID>-disk-0
# wait 2s, then:
sudo mount /dev/nbd1p4 /mnt/vm # p4 is the root partition on Rocky 9 GenericCloud
sudo tee -a /mnt/vm/home/wayne/.ssh/authorized_keys <<< 'ssh-ed25519 AAAA... extra-key'
sudo umount /mnt/vm
sudo qemu-nbd --disconnect /dev/nbd1
sudo qm start <VMID>
```
Rocky 9 GenericCloud partition layout: `p1`=BIOS boot (2M),
`p2`=EFI (100M), `p3`=/boot (1G), `p4`=/ (rest).
---
## Step 3 — First-boot system preparation
SSH in as `wayne` once cloud-init has completed (usually 60–90 s):
```bash
ssh wayne@<VM-IP>
```
### Add swap (required — FreeIPA needs headroom beyond 1.7 GB RAM)
```bash
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab
```
### Set static IP via NetworkManager
```bash
CON=$(nmcli -t -f NAME con show --active | head -1)
sudo nmcli con mod "$CON" \
ipv4.method manual \
ipv4.addresses 192.168.2.253/24 \
ipv4.gateway 192.168.2.254 \
ipv4.dns 192.168.2.254 \
ipv4.dns-search sweet.home
# Note: using the gateway as DNS here — after IPA installs it becomes the
# authoritative resolver at 192.168.2.253. Clients should then point to
# 192.168.2.253 for sweet.home resolution.
sudo nmcli con up "$CON"
```
### Fix /etc/hosts (cloud-init maps FQDN to 127.0.0.1 — IPA requires real IP)
```bash
sudo sed -i '/domain-controller/d' /etc/hosts
echo '192.168.2.253 domain-controller.sweet.home domain-controller' \
| sudo tee -a /etc/hosts
# Prevent cloud-init from resetting this on reboot
sudo sed -i 's/manage_etc_hosts: true/manage_etc_hosts: false/' \
/etc/cloud/cloud.cfg
```
---
## Step 4 — Install FreeIPA packages
```bash
sudo dnf install -y ipa-server ipa-server-dns
```
This pulls ~200 packages including 389-ds, Dogtag PKI, BIND, and MIT
Kerberos. Takes 5–10 minutes depending on mirror speed.
---
## Step 5 — Run the unattended install
Generate strong passwords (min 8 chars; store them in your password manager):
```bash
DM_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
ADMIN_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
echo "Directory Manager: $DM_PASS"
echo "IPA Admin: $ADMIN_PASS"
# Save both in your password manager NOW before proceeding
```
Run the installer (takes 15–20 minutes):
```bash
sudo ipa-server-install \
--realm=SWEET.HOME \
--domain=sweet.home \
--hostname=domain-controller.sweet.home \
--ds-password="$DM_PASS" \
--admin-password="$ADMIN_PASS" \
--setup-dns \
--forwarder=192.168.2.254 \
--no-dnssec-validation \
--no-ntp \
--unattended
```
Key flags:
- `--setup-dns` — install BIND as IPA's authoritative DNS for `sweet.home`
- `--forwarder=192.168.2.254` — forward non-sweet.home queries to the LAN gateway
- `--no-dnssec-validation` — skip DNSSEC (home lab has no DNSSEC chain)
- `--no-ntp` — Proxmox handles time sync for guests; don't install chrony
---
## Step 6 — Verify
```bash
# All services should show RUNNING
ipactl status
# Get a Kerberos ticket and confirm
echo "$ADMIN_PASS" | kinit admin
klist
# Check DNS SRV records are in place
dig +short _kerberos._udp.sweet.home SRV @127.0.0.1
# Expected: 0 100 88 domain-controller.sweet.home.
```
---
## Step 7 — Back up the CA certificate
```bash
# On domain-controller (encrypted with Directory Manager password)
ls -lh /root/cacert.p12
# Copy to a safe location
scp root@domain-controller:/root/cacert.p12 ~/backups/ipa-cacert.p12
```
This file is required if you ever set up a replica or need to
re-issue service certificates.
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# FreeIPA server install script for domain-controller.sweet.home
#
# Run this on a fresh Rocky Linux 9 VM that already has:
# - Correct hostname: domain-controller.sweet.home
# - Static IP: 192.168.2.138/24
# - Gateway: 192.168.2.254
# - DNS: 192.168.2.253 (Pi-hole)
# - sudo access for the current user
#
# The script will prompt for passwords if not set via environment:
# IPA_DM_PASSWORD Directory Manager password (store in password manager)
# IPA_ADMIN_PASSWORD IPA admin Kerberos password (store in password manager)
#
# Full procedure: see docs/install.md
set -euo pipefail
IPA_REALM="SWEET.HOME"
IPA_DOMAIN="sweet.home"
IPA_HOSTNAME="domain-controller.sweet.home"
IPA_IP="192.168.2.138"
IPA_DNS_FORWARDER="192.168.2.253"
# ── Password handling ────────────────────────────────────────────────────────
if [[ -z "${IPA_DM_PASSWORD:-}" ]]; then
read -r -s -p "Directory Manager password (min 8 chars): " IPA_DM_PASSWORD
echo
fi
if [[ -z "${IPA_ADMIN_PASSWORD:-}" ]]; then
read -r -s -p "IPA admin password (min 8 chars): " IPA_ADMIN_PASSWORD
echo
fi
if [[ ${#IPA_DM_PASSWORD} -lt 8 || ${#IPA_ADMIN_PASSWORD} -lt 8 ]]; then
echo "ERROR: passwords must be at least 8 characters" >&2
exit 1
fi
# ── Pre-flight checks ────────────────────────────────────────────────────────
echo "==> Checking hostname..."
actual_fqdn=$(hostname -f)
if [[ "$actual_fqdn" != "$IPA_HOSTNAME" ]]; then
echo "ERROR: hostname -f returned '$actual_fqdn', expected '$IPA_HOSTNAME'" >&2
exit 1
fi
echo "==> Checking /etc/hosts entry..."
if ! grep -q "$IPA_IP $IPA_HOSTNAME" /etc/hosts; then
echo "ERROR: /etc/hosts does not have '$IPA_IP $IPA_HOSTNAME'" >&2
echo "Fix: sudo sed -i '/$IPA_HOSTNAME/d' /etc/hosts && echo '$IPA_IP $IPA_HOSTNAME ${IPA_HOSTNAME%%.*}' | sudo tee -a /etc/hosts" >&2
exit 1
fi
echo "==> Checking Python FQDN resolution..."
resolved=$(python3 -c "import socket; print(socket.gethostbyname('$IPA_HOSTNAME'))" 2>/dev/null || true)
if [[ "$resolved" != "$IPA_IP" ]]; then
echo "ERROR: $IPA_HOSTNAME resolves to '$resolved', expected '$IPA_IP'" >&2
exit 1
fi
echo "==> Checking internet connectivity..."
if ! ping -c1 -W5 8.8.8.8 >/dev/null 2>&1; then
echo "ERROR: no internet connectivity (needed for package install)" >&2
exit 1
fi
# ── Swap ─────────────────────────────────────────────────────────────────────
if ! swapon --show | grep -q .; then
echo "==> Creating 2 GB swap file (FreeIPA needs headroom)..."
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab
else
echo "==> Swap already configured, skipping."
fi
# ── Packages ─────────────────────────────────────────────────────────────────
echo "==> Installing FreeIPA server packages..."
sudo dnf install -y ipa-server ipa-server-dns
# ── Install ──────────────────────────────────────────────────────────────────
echo "==> Running ipa-server-install (15–20 min)..."
sudo ipa-server-install \
--realm="$IPA_REALM" \
--domain="$IPA_DOMAIN" \
--hostname="$IPA_HOSTNAME" \
--ds-password="$IPA_DM_PASSWORD" \
--admin-password="$IPA_ADMIN_PASSWORD" \
--setup-dns \
--forwarder="$IPA_DNS_FORWARDER" \
--no-dnssec-validation \
--no-ntp \
--unattended
# ── Verify ───────────────────────────────────────────────────────────────────
echo "==> Verifying services..."
sudo ipactl status
echo "==> Verifying Kerberos ticket..."
echo "$IPA_ADMIN_PASSWORD" | kinit admin
klist
echo "==> Verifying DNS SRV records..."
dig +short _kerberos._udp."$IPA_DOMAIN" SRV @127.0.0.1
echo ""
echo "======================================================================"
echo "Setup complete. Next steps:"
echo ""
echo " 1. Save passwords to your password manager (if not already done)."
echo ""
echo " 2. Configure Pi-hole to forward $IPA_DOMAIN DNS to $IPA_IP:"
echo " bash scripts/configure-pihole-dns.sh <pihole-host>"
echo ""
echo " 3. Back up the CA certificates:"
echo " scp root@$IPA_HOSTNAME:/root/cacert.p12 ~/backups/ipa-cacert.p12"
echo " (Encrypted with the Directory Manager password)"
echo ""
echo " 4. Access the Web UI at:"
echo " https://$IPA_HOSTNAME/ipa/ui/"
echo "======================================================================"
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Read-only health check for the FreeIPA server.
# Run locally on domain-controller or remotely:
# ssh wayne@domain-controller 'bash -s' < scripts/verify.sh
#
# Exit code 0 = all checks passed, non-zero = something is wrong.
set -uo pipefail
PASS=0
FAIL=0
check() {
local label="$1"
shift
if "$@" >/dev/null 2>&1; then
echo " OK $label"
(( PASS++ )) || true
else
echo "FAIL $label"
(( FAIL++ )) || true
fi
}
echo "=== FreeIPA health check: $(hostname -f) ==="
echo ""
echo "--- Services ---"
check "ipactl status" sudo ipactl status
check "dirsrv running" systemctl is-active dirsrv.target
check "krb5kdc running" systemctl is-active krb5kdc
check "named running" systemctl is-active named
check "httpd running" systemctl is-active httpd
check "pki-tomcatd running" systemctl is-active pki-tomcatd.target
echo ""
echo "--- DNS ---"
check "A record: domain-controller.sweet.home" dig +short domain-controller.sweet.home A @127.0.0.1
check "SRV: _kerberos._udp.sweet.home" dig +short _kerberos._udp.sweet.home SRV @127.0.0.1
check "SRV: _ldap._tcp.sweet.home" dig +short _ldap._tcp.sweet.home SRV @127.0.0.1
check "TXT: _kerberos.sweet.home" dig +short _kerberos.sweet.home TXT @127.0.0.1
echo ""
echo "--- LDAP ---"
check "LDAP port 389 open" bash -c "exec 3<>/dev/tcp/127.0.0.1/389"
check "LDAPS port 636 open" bash -c "exec 3<>/dev/tcp/127.0.0.1/636"
echo ""
echo "--- Kerberos ---"
check "KDC port 88 open" bash -c "exec 3<>/dev/tcp/127.0.0.1/88"
echo ""
echo "--- HTTP ---"
check "IPA HTTP redirect" curl -sk -o /dev/null -w "%{http_code}" http://localhost/ | grep -qE "^(301|302|200)"
check "IPA HTTPS UI" curl -sk -o /dev/null -w "%{http_code}" https://localhost/ipa/ui/ | grep -q "200"
echo ""
if [[ $FAIL -eq 0 ]]; then
echo "All $PASS checks passed."
else
echo "$FAIL check(s) FAILED, $PASS passed."
exit 1
fi
+6
View File
@@ -65,3 +65,9 @@ fi
echo " - ${SCRIPT_DIR}/create-admin-user.sh <username> (PVE web UI account)" echo " - ${SCRIPT_DIR}/create-admin-user.sh <username> (PVE web UI account)"
echo " - Enable 2FA/TOTP for that user and root@pam via the web UI" echo " - Enable 2FA/TOTP for that user and root@pam via the web UI"
echo " - ${SCRIPT_DIR}/audit.sh (verify everything above)" echo " - ${SCRIPT_DIR}/audit.sh (verify everything above)"
echo
echo " If this host will be enrolled in FreeIPA:"
echo " ipa-client-install --domain=sweet.home --realm=SWEET.HOME \\"
echo " --server=domain-controller.sweet.home --mkhomedir --ssh-trust-dns --no-ntp"
echo " ${SCRIPT_DIR}/setup-ipa-sudo.sh (NOPASSWD sudo for IPA admins group)"
echo " ${SCRIPT_DIR}/create-local-backdoor.sh <ssh-pubkey> (emergency local account)"
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# Create a local 'pveadmin' account as an emergency backdoor for when
# IPA/SSSD is unavailable. The account authenticates by SSH key only
# (password auth is disabled by harden-ssh.sh); the password set here
# is for physical console access only.
#
# Idempotent - safe to re-run. If the account already exists, the SSH key
# is refreshed but the password and account are left unchanged. Run as root.
#
# Usage:
# ./create-local-backdoor.sh <ssh-public-key>
# ./create-local-backdoor.sh --key-file <path-to-.pub>
#
# Set BACKDOOR_PASS env var to supply the console password non-interactively;
# otherwise you will be prompted.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
require_root
USERNAME="pveadmin"
SSH_KEY=""
if [ "${1:-}" = "--key-file" ]; then
KEY_FILE="${2:-}"
[ -z "$KEY_FILE" ] && { echo "ERROR: --key-file requires a path." >&2; exit 1; }
[ -f "$KEY_FILE" ] || { echo "ERROR: key file not found: $KEY_FILE" >&2; exit 1; }
SSH_KEY="$(cat "$KEY_FILE")"
else
SSH_KEY="${1:-}"
fi
if [ -z "$SSH_KEY" ]; then
echo "Usage: $0 <ssh-public-key>" >&2
echo " $0 --key-file <path-to-.pub>" >&2
exit 1
fi
if ! echo "$SSH_KEY" | grep -qE '^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) [A-Za-z0-9+/=]'; then
echo "ERROR: argument doesn't look like a valid SSH public key." >&2
exit 1
fi
if id "$USERNAME" >/dev/null 2>&1; then
echo "User '${USERNAME}' already exists -- not modifying account or password."
else
useradd --create-home --shell /bin/bash "$USERNAME"
echo "Created Linux user '${USERNAME}'."
if [ -n "${BACKDOOR_PASS:-}" ]; then
printf '%s:%s\n' "$USERNAME" "$BACKDOOR_PASS" | chpasswd
echo "Console password set."
else
echo "Set a console password for '${USERNAME}' (used for physical console access only):"
passwd "$USERNAME"
fi
fi
HOME_DIR="$(getent passwd "$USERNAME" | cut -d: -f6)"
SSH_DIR="${HOME_DIR}/.ssh"
AUTH_FILE="${SSH_DIR}/authorized_keys"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
chown "${USERNAME}:${USERNAME}" "$SSH_DIR"
if grep -qF "$SSH_KEY" "$AUTH_FILE" 2>/dev/null; then
echo "SSH key already present in ${AUTH_FILE}."
else
printf '%s\n' "$SSH_KEY" >> "$AUTH_FILE"
echo "Installed SSH key in ${AUTH_FILE}."
fi
chmod 600 "$AUTH_FILE"
chown "${USERNAME}:${USERNAME}" "$AUTH_FILE"
SUDOERS_FILE="/etc/sudoers.d/${USERNAME}-nopasswd"
write_if_changed "$SUDOERS_FILE" "${USERNAME} ALL=(root) NOPASSWD: ALL"
chmod 0440 "$SUDOERS_FILE"
visudo -c >/dev/null
echo "Sudoers rule for '${USERNAME}' is valid."
echo
echo "'${USERNAME}' is ready: SSH key login, NOPASSWD sudo, console password set."
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# Grant the IPA 'admins' group passwordless sudo on this host.
#
# Writes two files:
# /etc/sudoers.d/admins-nopasswd -- NOPASSWD: ALL for general shell use
# /etc/sudoers.d/admins-proxmox -- NOPASSWD for pvesh/qm/pct (PVE only;
# skipped silently if those binaries
# aren't present, e.g. on PBS/PDM)
#
# Idempotent - safe to re-run. Run as root on the target host after
# ipa-client-install has been completed and SSSD is active.
#
# Usage: ./setup-ipa-sudo.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
require_root
if [ ! -f /etc/ipa/default.conf ]; then
echo "ERROR: /etc/ipa/default.conf not found -- is this host enrolled in FreeIPA?" >&2
exit 1
fi
write_if_changed /etc/sudoers.d/admins-nopasswd '%admins ALL=(root) NOPASSWD: ALL'
chmod 0440 /etc/sudoers.d/admins-nopasswd
# PVE-specific tools -- pvesh moved from /usr/sbin to /usr/bin in PVE 8.x;
# resolve at script time so the path in the sudoers rule is always correct.
PVESH="$(command -v pvesh 2>/dev/null || true)"
QM="$(command -v qm 2>/dev/null || true)"
PCT="$(command -v pct 2>/dev/null || true)"
if [ -n "$PVESH" ] && [ -n "$QM" ] && [ -n "$PCT" ]; then
write_if_changed /etc/sudoers.d/admins-proxmox \
"%admins ALL=(root) NOPASSWD: ${PVESH}, ${QM}, ${PCT}"
chmod 0440 /etc/sudoers.d/admins-proxmox
echo "Proxmox tools found -- wrote admins-proxmox."
else
echo "pvesh/qm/pct not found -- skipping admins-proxmox (not a PVE host)."
fi
visudo -c >/dev/null
echo "All sudoers files valid. IPA admins group has sudo on this host."
+39
View File
@@ -0,0 +1,39 @@
# raspberrypi CLAUDE.md
Guardrails for Claude Code working on raspberrypi configuration.
## Host
`raspberrypi.tail13f623.ts.net` (100.86.56.87) — Raspberry Pi 4, Debian 12
bookworm (aarch64). Reachable from LAN via Tailscale MagicDNS.
## Production status
The Raspberry Pi is a **production host** running live services (Traefik,
Uptime Kuma, CrowdSec, Beszel agent). Treat it the same as pve1: read-only
inspection is always fine; any script that writes to the host requires
explicit same-session operator go-ahead.
## Bootstrap access
The local `raspi` account has NOPASSWD sudo and the nixos ED25519 key
authorized. Use it to bootstrap IPA sudo rules or make root-level changes
when `wayne` sudo is not yet working.
## IPA integration
- Enrolled in `SWEET.HOME` realm via `ipa-client-install`.
- SSSD resolves IPA groups: `admins (50000)`, `docker-access (50010)`.
- `%admins NOPASSWD:ALL` granted via `/etc/sudoers.d/ipa-admins`
(written by `scripts/setup-ipa-sudo.sh`).
## Docker GID
The local `docker` group GID is pinned to 50010 (`groupmod --non-unique`)
so it matches the IPA `docker-access` group. Members of `docker-access`
in IPA can run docker without any per-host group membership entry.
Applied by `scripts/setup-docker-ipa-gid.sh`.
## What must never be committed
SSH private keys, passwords, API tokens, Tailscale auth keys.
+52
View File
@@ -0,0 +1,52 @@
# raspberrypi
Configuration scripts for `raspberrypi.tail13f623.ts.net` — Raspberry Pi 4
running Debian 12 bookworm (aarch64). Reachable from LAN via Tailscale
MagicDNS (`tail13f623.ts.net`).
## Services
- **Traefik** — reverse proxy (ports 80, 443, 8080)
- **Uptime Kuma** — uptime monitoring
- **CrowdSec** — intrusion detection
- **Beszel agent** — metrics collection
## IPA enrollment
The Pi is enrolled in the `sweet.home` FreeIPA domain. SSSD resolves:
- `admins (GID 50000)` — sudo access
- `docker-access (GID 50010)` — docker socket access
## Setup scripts
Run these **as root** (or via `sudo`) after `ipa-client-install` completes.
### 1. IPA sudo
```bash
sudo ./scripts/setup-ipa-sudo.sh
```
Writes `/etc/sudoers.d/ipa-admins` granting `%admins NOPASSWD:ALL`. After
this, IPA users in the `admins` group can `sudo` without a password.
### 2. Docker GID
```bash
sudo ./scripts/setup-docker-ipa-gid.sh
```
Pins the local `docker` group GID to 50010 to match the IPA `docker-access`
group. Restarts `docker.socket` + `docker.service` to recreate the socket
with the new GID. After this, IPA members of `docker-access` can run docker
without any per-host group membership entry.
## Current status
| Item | Status |
|---|---|
| IPA enrollment | ✅ |
| SSH (wayne) | ✅ via `raspberrypi.tail13f623.ts.net` |
| Sudo (wayne, NOPASSWD) | ✅ `/etc/sudoers.d/ipa-admins` |
| Docker (wayne, via IPA group) | ✅ docker group GID = 50010 |
| Bootstrap sudo | ✅ local `raspi` user, NOPASSWD |
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Shared helpers for raspberrypi scripts. Sourced, not executed directly:
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# source "${SCRIPT_DIR}/lib/common.sh"
require_root() {
if [ "$(id -u)" -ne 0 ]; then
echo "Must run as root." >&2
exit 1
fi
}
# backup_file <path>
# Copies an existing file to <path>.bak.<epoch>. No-op if it doesn't exist.
backup_file() {
local path="$1"
if [ -f "$path" ]; then
cp "$path" "${path}.bak.$(date +%s)"
echo "Backed up ${path}"
fi
}
# write_if_changed <path> <content>
# Writes content to path only if it differs from what's already there,
# backing up the previous version first. Prints what happened.
write_if_changed() {
local path="$1" content="$2"
if [ -f "$path" ] && [ "$(cat "$path")" = "$content" ]; then
echo "Already up to date: $path"
return 0
fi
backup_file "$path"
printf '%s\n' "$content" > "$path"
echo "Wrote $path"
}
@@ -0,0 +1,53 @@
#!/bin/bash
# Pin the local 'docker' group GID to match the IPA 'docker-access' group
# (GID 50010) so that IPA group membership alone grants docker socket access.
#
# On Debian, GID 50010 is already present via SSSD (the IPA group), so
# groupmod requires --non-unique to allow the local docker group to share
# it. The docker.socket + docker.service pair must be fully stopped before
# removing the stale socket so systemd recreates it with the new GID.
#
# Idempotent — exits 0 without touching anything if the GID is already 50010.
#
# Run as root after setup-ipa-sudo.sh has been applied and docker is running.
#
# Usage: sudo ./setup-docker-ipa-gid.sh
set -euo pipefail
DOCKER_ACCESS_GID=50010
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
require_root
if ! command -v docker &>/dev/null; then
echo "ERROR: docker not found -- install Docker before running this script." >&2
exit 1
fi
current_gid=$(getent group docker | cut -d: -f3)
if [ "$current_gid" = "$DOCKER_ACCESS_GID" ]; then
echo "docker group is already GID ${DOCKER_ACCESS_GID} -- nothing to do."
exit 0
fi
echo "Changing docker group GID: ${current_gid} -> ${DOCKER_ACCESS_GID}"
# SSSD exposes GID 50010 via the IPA docker-access group, so groupmod sees
# it as already in use. --non-unique lets the local docker group share it.
groupmod --non-unique -g "${DOCKER_ACCESS_GID}" docker
echo "Restarting docker to recreate socket with new GID..."
systemctl stop docker.service docker.socket
rm -f /var/run/docker.sock
systemctl start docker.socket docker.service
actual_gid=$(stat -c '%g' /var/run/docker.sock)
if [ "$actual_gid" != "$DOCKER_ACCESS_GID" ]; then
echo "ERROR: socket GID is ${actual_gid}, expected ${DOCKER_ACCESS_GID}" >&2
exit 1
fi
echo "Done. Docker socket is now GID ${DOCKER_ACCESS_GID} (docker / docker-access)."
echo "IPA members of the 'docker-access' group can now run docker without explicit local group membership."
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Grant the IPA 'admins' group passwordless sudo on this Raspberry Pi.
#
# Writes /etc/sudoers.d/ipa-admins with NOPASSWD: ALL for the admins group.
# Idempotent — safe to re-run.
#
# Run as root (or via the local 'raspi' user's NOPASSWD sudo) after
# ipa-client-install has been completed and SSSD is active.
#
# Usage: sudo ./setup-ipa-sudo.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
require_root
if [ ! -f /etc/ipa/default.conf ]; then
echo "ERROR: /etc/ipa/default.conf not found -- is this host enrolled in FreeIPA?" >&2
exit 1
fi
write_if_changed /etc/sudoers.d/ipa-admins '%admins ALL=(ALL) NOPASSWD:ALL'
chmod 0440 /etc/sudoers.d/ipa-admins
visudo -c >/dev/null
echo "Sudoers file valid. IPA admins group has NOPASSWD sudo on this host."