From afabcbbeff3b0ddc7209eb95e88bc813506c5a17 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 15:45:58 +1000 Subject: [PATCH] feat(raspberrypi): add Raspberry Pi configuration section 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 --- CLAUDE.md | 10 ++-- README.md | 16 +++++-- raspberrypi/CLAUDE.md | 39 +++++++++++++++ raspberrypi/README.md | 52 ++++++++++++++++++++ raspberrypi/scripts/lib/common.sh | 35 ++++++++++++++ raspberrypi/scripts/setup-docker-ipa-gid.sh | 53 +++++++++++++++++++++ raspberrypi/scripts/setup-ipa-sudo.sh | 27 +++++++++++ 7 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 raspberrypi/CLAUDE.md create mode 100644 raspberrypi/README.md create mode 100644 raspberrypi/scripts/lib/common.sh create mode 100644 raspberrypi/scripts/setup-docker-ipa-gid.sh create mode 100644 raspberrypi/scripts/setup-ipa-sudo.sh diff --git a/CLAUDE.md b/CLAUDE.md index 22d0245..23fdda7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,9 +6,9 @@ OVERRIDE any default behaviour and must be followed exactly. ## Repo purpose Configuration toolsets and docs for Wayne's Debian-based LAN machines. -Three sections currently exist: `proxmox/` (Proxmox VE hosts), -`pihole/` (Pi-hole DNS/DHCP), and `freeipa/` (FreeIPA identity -management). Each has its own README. +Four sections currently exist: `proxmox/` (Proxmox VE hosts), +`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) @@ -40,6 +40,10 @@ management). Each has its own README. 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 diff --git a/README.md b/README.md index 48d09db..cd3aa6a 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ service type. ## Layout ``` -proxmox/ Proxmox VE hosts (pve1 production, pve-test sandbox) -pihole/ Pi-hole DNS/DHCP (config snapshots, pull/apply scripts) -freeipa/ FreeIPA identity management server (domain-controller.sweet.home) -scripts/ Repo-wide scripts (secret scanning, git hook installer) +proxmox/ Proxmox VE hosts (pve1 production, pve-test sandbox) +pihole/ Pi-hole DNS/DHCP (config snapshots, pull/apply scripts) +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 @@ -34,6 +35,13 @@ 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 All commits are scanned for secrets by a CI pipeline that runs on both diff --git a/raspberrypi/CLAUDE.md b/raspberrypi/CLAUDE.md new file mode 100644 index 0000000..15a54af --- /dev/null +++ b/raspberrypi/CLAUDE.md @@ -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. diff --git a/raspberrypi/README.md b/raspberrypi/README.md new file mode 100644 index 0000000..2dd8d87 --- /dev/null +++ b/raspberrypi/README.md @@ -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 | diff --git a/raspberrypi/scripts/lib/common.sh b/raspberrypi/scripts/lib/common.sh new file mode 100644 index 0000000..6eb3514 --- /dev/null +++ b/raspberrypi/scripts/lib/common.sh @@ -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 +# Copies an existing file to .bak.. 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 +# 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" +} diff --git a/raspberrypi/scripts/setup-docker-ipa-gid.sh b/raspberrypi/scripts/setup-docker-ipa-gid.sh new file mode 100644 index 0000000..6769195 --- /dev/null +++ b/raspberrypi/scripts/setup-docker-ipa-gid.sh @@ -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." diff --git a/raspberrypi/scripts/setup-ipa-sudo.sh b/raspberrypi/scripts/setup-ipa-sudo.sh new file mode 100644 index 0000000..b0703ce --- /dev/null +++ b/raspberrypi/scripts/setup-ipa-sudo.sh @@ -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."