Archived
Compare commits
6
Commits
a6a419bad7
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afabcbbeff | ||
|
|
a4d907e2eb | ||
|
|
761fe9e194 | ||
|
|
0c15de3329 | ||
|
|
f54e00ebc8 | ||
|
|
89cc8c8cc3 |
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
Executable
+130
@@ -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 "======================================================================"
|
||||||
Executable
+63
@@ -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
|
||||||
@@ -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)"
|
||||||
Executable
+84
@@ -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."
|
||||||
Executable
+45
@@ -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."
|
||||||
@@ -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.
|
||||||
@@ -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 |
|
||||||
@@ -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."
|
||||||
@@ -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."
|
||||||
Reference in new issue
Block a user