Archived
Compare commits
4
Commits
f54e00ebc8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afabcbbeff | ||
|
|
a4d907e2eb | ||
|
|
761fe9e194 | ||
|
|
0c15de3329 |
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,10 +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)
|
||||||
freeipa/ FreeIPA identity management server (domain-controller.sweet.home)
|
freeipa/ FreeIPA identity management server (domain-controller.sweet.home)
|
||||||
scripts/ Repo-wide scripts (secret scanning, git hook installer)
|
raspberrypi/ Raspberry Pi 4 (Debian 12, IPA-enrolled, Docker host)
|
||||||
|
scripts/ Repo-wide scripts (secret scanning, git hook installer)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sections
|
## 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`
|
`freeipa/README.md` for the quick-start and `freeipa/docs/install.md`
|
||||||
for the full reproduction procedure.
|
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
|
||||||
|
|||||||
+3
-16
@@ -9,7 +9,7 @@ Proxmox VM (VMID 108 on `pve1.sweet.home`).
|
|||||||
| Item | Value |
|
| Item | Value |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| Host | `domain-controller.sweet.home` |
|
| Host | `domain-controller.sweet.home` |
|
||||||
| IP | `192.168.2.138` (static) |
|
| IP | `192.168.2.253` (static) |
|
||||||
| Realm | `SWEET.HOME` |
|
| Realm | `SWEET.HOME` |
|
||||||
| Domain | `sweet.home` |
|
| Domain | `sweet.home` |
|
||||||
| IPA version | 4.13.x (Rocky Linux 9) |
|
| IPA version | 4.13.x (Rocky Linux 9) |
|
||||||
@@ -21,8 +21,8 @@ Proxmox VM (VMID 108 on `pve1.sweet.home`).
|
|||||||
|
|
||||||
- **Kerberos KDC** — SSO tickets for the `SWEET.HOME` realm
|
- **Kerberos KDC** — SSO tickets for the `SWEET.HOME` realm
|
||||||
- **LDAP directory** — centralised user/group/host store (389-ds)
|
- **LDAP directory** — centralised user/group/host store (389-ds)
|
||||||
- **Integrated DNS** — authoritative for `sweet.home`, forwarding
|
- **Integrated DNS** — authoritative for `sweet.home` and primary LAN
|
||||||
everything else upstream to Pi-hole (`192.168.2.253`)
|
resolver for all hosts, forwarding everything else to the LAN gateway
|
||||||
- **CA** — self-signed CA issuing certs for IPA services
|
- **CA** — self-signed CA issuing certs for IPA services
|
||||||
- **Web UI** — at `https://domain-controller.sweet.home/ipa/ui/`
|
- **Web UI** — at `https://domain-controller.sweet.home/ipa/ui/`
|
||||||
|
|
||||||
@@ -60,19 +60,6 @@ kinit admin
|
|||||||
ipa passwd admin
|
ipa passwd admin
|
||||||
```
|
```
|
||||||
|
|
||||||
## DNS integration with Pi-hole
|
|
||||||
|
|
||||||
Pi-hole must forward `sweet.home` DNS queries to this server so
|
|
||||||
that Kerberos SRV records and IPA host records resolve correctly.
|
|
||||||
Run `scripts/configure-pihole-dns.sh` once, or add the following
|
|
||||||
to Pi-hole's custom DNS config manually:
|
|
||||||
|
|
||||||
```
|
|
||||||
server=/sweet.home/192.168.2.138
|
|
||||||
```
|
|
||||||
|
|
||||||
See `docs/pihole-dns.md` for the full setup.
|
|
||||||
|
|
||||||
## Ports required (firewalld)
|
## Ports required (firewalld)
|
||||||
|
|
||||||
FreeIPA's firewalld config is applied by `ipa-server-install` automatically.
|
FreeIPA's firewalld config is applied by `ipa-server-install` automatically.
|
||||||
|
|||||||
+12
-18
@@ -78,11 +78,13 @@ echo 'ssh-rsa AAAA... wayne@stream' > /tmp/admin-key.pub
|
|||||||
# (use the key from variables.nix adminSshKey)
|
# (use the key from variables.nix adminSshKey)
|
||||||
|
|
||||||
# Configure cloud-init
|
# 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> \
|
sudo qm set <VMID> \
|
||||||
--ciuser wayne \
|
--ciuser wayne \
|
||||||
--sshkeys /tmp/admin-key.pub \
|
--sshkeys /tmp/admin-key.pub \
|
||||||
--ipconfig0 ip=dhcp \
|
--ipconfig0 ip=dhcp \
|
||||||
--nameserver 192.168.2.253 \
|
--nameserver 192.168.2.254 \
|
||||||
--searchdomain sweet.home
|
--searchdomain sweet.home
|
||||||
|
|
||||||
# Set boot order
|
# Set boot order
|
||||||
@@ -138,10 +140,13 @@ echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab
|
|||||||
CON=$(nmcli -t -f NAME con show --active | head -1)
|
CON=$(nmcli -t -f NAME con show --active | head -1)
|
||||||
sudo nmcli con mod "$CON" \
|
sudo nmcli con mod "$CON" \
|
||||||
ipv4.method manual \
|
ipv4.method manual \
|
||||||
ipv4.addresses 192.168.2.138/24 \
|
ipv4.addresses 192.168.2.253/24 \
|
||||||
ipv4.gateway 192.168.2.254 \
|
ipv4.gateway 192.168.2.254 \
|
||||||
ipv4.dns 192.168.2.253 \
|
ipv4.dns 192.168.2.254 \
|
||||||
ipv4.dns-search sweet.home
|
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"
|
sudo nmcli con up "$CON"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -149,7 +154,7 @@ sudo nmcli con up "$CON"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo sed -i '/domain-controller/d' /etc/hosts
|
sudo sed -i '/domain-controller/d' /etc/hosts
|
||||||
echo '192.168.2.138 domain-controller.sweet.home domain-controller' \
|
echo '192.168.2.253 domain-controller.sweet.home domain-controller' \
|
||||||
| sudo tee -a /etc/hosts
|
| sudo tee -a /etc/hosts
|
||||||
|
|
||||||
# Prevent cloud-init from resetting this on reboot
|
# Prevent cloud-init from resetting this on reboot
|
||||||
@@ -192,7 +197,7 @@ sudo ipa-server-install \
|
|||||||
--ds-password="$DM_PASS" \
|
--ds-password="$DM_PASS" \
|
||||||
--admin-password="$ADMIN_PASS" \
|
--admin-password="$ADMIN_PASS" \
|
||||||
--setup-dns \
|
--setup-dns \
|
||||||
--forwarder=192.168.2.253 \
|
--forwarder=192.168.2.254 \
|
||||||
--no-dnssec-validation \
|
--no-dnssec-validation \
|
||||||
--no-ntp \
|
--no-ntp \
|
||||||
--unattended
|
--unattended
|
||||||
@@ -200,7 +205,7 @@ sudo ipa-server-install \
|
|||||||
|
|
||||||
Key flags:
|
Key flags:
|
||||||
- `--setup-dns` — install BIND as IPA's authoritative DNS for `sweet.home`
|
- `--setup-dns` — install BIND as IPA's authoritative DNS for `sweet.home`
|
||||||
- `--forwarder=192.168.2.253` — forward non-sweet.home queries to Pi-hole
|
- `--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-dnssec-validation` — skip DNSSEC (home lab has no DNSSEC chain)
|
||||||
- `--no-ntp` — Proxmox handles time sync for guests; don't install chrony
|
- `--no-ntp` — Proxmox handles time sync for guests; don't install chrony
|
||||||
|
|
||||||
@@ -223,18 +228,7 @@ dig +short _kerberos._udp.sweet.home SRV @127.0.0.1
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 7 — Configure Pi-hole to forward sweet.home DNS
|
## Step 7 — Back up the CA certificate
|
||||||
|
|
||||||
See `docs/pihole-dns.md` and `scripts/configure-pihole-dns.sh`.
|
|
||||||
|
|
||||||
The short version — add to Pi-hole's custom dnsmasq config:
|
|
||||||
```
|
|
||||||
server=/sweet.home/192.168.2.138
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 8 — Back up the CA certificate
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On domain-controller (encrypted with Directory Manager password)
|
# On domain-controller (encrypted with Directory Manager password)
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
# Pi-hole DNS forwarding for sweet.home
|
|
||||||
|
|
||||||
FreeIPA's BIND instance is the authoritative DNS server for the
|
|
||||||
`sweet.home` zone. Pi-hole (at `192.168.2.253`) is the LAN resolver
|
|
||||||
for everything else. To make Kerberos, LDAP, and host lookups work
|
|
||||||
from all LAN clients, Pi-hole must forward `sweet.home` queries to
|
|
||||||
the domain-controller.
|
|
||||||
|
|
||||||
## What needs configuring
|
|
||||||
|
|
||||||
One dnsmasq directive on Pi-hole:
|
|
||||||
|
|
||||||
```
|
|
||||||
server=/sweet.home/192.168.2.138
|
|
||||||
```
|
|
||||||
|
|
||||||
This makes Pi-hole forward every `*.sweet.home` query (and the bare
|
|
||||||
`sweet.home` zone) to `192.168.2.138` (domain-controller) instead
|
|
||||||
of its upstream resolver.
|
|
||||||
|
|
||||||
## How to apply
|
|
||||||
|
|
||||||
Use `scripts/configure-pihole-dns.sh` for an automated, idempotent
|
|
||||||
application, or apply manually as below.
|
|
||||||
|
|
||||||
### Manual — Pi-hole v6 (TOML config)
|
|
||||||
|
|
||||||
Pi-hole v6 reads custom dnsmasq options from:
|
|
||||||
```
|
|
||||||
/etc/pihole/pihole.toml → dns.customDnsEntries
|
|
||||||
```
|
|
||||||
or from drop-in files in `/etc/dnsmasq.d/`. The drop-in approach
|
|
||||||
is more robust across upgrades:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh <pihole-host>
|
|
||||||
echo 'server=/sweet.home/192.168.2.138' \
|
|
||||||
| sudo tee /etc/dnsmasq.d/10-ipa-sweet-home.conf
|
|
||||||
sudo pihole restartdns
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
```bash
|
|
||||||
dig +short _kerberos._udp.sweet.home SRV @192.168.2.253
|
|
||||||
# Should return: 0 100 88 domain-controller.sweet.home.
|
|
||||||
dig +short domain-controller.sweet.home A @192.168.2.253
|
|
||||||
# Should return: 192.168.2.138
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why this is required
|
|
||||||
|
|
||||||
FreeIPA clients use DNS SRV records to discover the KDC, LDAP server,
|
|
||||||
and other IPA services. Without the forwarder, Pi-hole returns NXDOMAIN
|
|
||||||
for `_kerberos._udp.sweet.home` and `_ldap._tcp.sweet.home`, which
|
|
||||||
causes `kinit` and IPA client enrollment to fail — even if the IP
|
|
||||||
address is reachable.
|
|
||||||
|
|
||||||
## Records IPA publishes
|
|
||||||
|
|
||||||
Once forwarding is set up, Pi-hole clients will be able to resolve:
|
|
||||||
|
|
||||||
| Record | Type | Value |
|
|
||||||
|--------|------|-------|
|
|
||||||
| `domain-controller.sweet.home` | A | `192.168.2.138` |
|
|
||||||
| `_kerberos._udp.sweet.home` | SRV | `0 100 88 domain-controller.sweet.home` |
|
|
||||||
| `_kerberos._tcp.sweet.home` | SRV | `0 100 88 domain-controller.sweet.home` |
|
|
||||||
| `_ldap._tcp.sweet.home` | SRV | `0 100 389 domain-controller.sweet.home` |
|
|
||||||
| `_kpasswd._udp.sweet.home` | SRV | `0 100 464 domain-controller.sweet.home` |
|
|
||||||
| `_kerberos.sweet.home` | TXT | `"SWEET.HOME"` |
|
|
||||||
| `ipa-ca.sweet.home` | A | `192.168.2.138` |
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Configure Pi-hole to forward sweet.home DNS queries to FreeIPA.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# bash configure-pihole-dns.sh <pihole-host>
|
|
||||||
# bash configure-pihole-dns.sh pihole.sweet.home
|
|
||||||
#
|
|
||||||
# Idempotent — safe to run multiple times.
|
|
||||||
# Requires SSH access to the Pi-hole host as a user with sudo.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
PIHOLE_HOST="${1:-}"
|
|
||||||
IPA_IP="192.168.2.138"
|
|
||||||
IPA_DOMAIN="sweet.home"
|
|
||||||
DNSMASQ_CONF="/etc/dnsmasq.d/10-ipa-${IPA_DOMAIN//./-}.conf"
|
|
||||||
DIRECTIVE="server=/${IPA_DOMAIN}/${IPA_IP}"
|
|
||||||
|
|
||||||
if [[ -z "$PIHOLE_HOST" ]]; then
|
|
||||||
echo "Usage: $0 <pihole-host>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> Configuring Pi-hole at $PIHOLE_HOST to forward $IPA_DOMAIN → $IPA_IP..."
|
|
||||||
|
|
||||||
ssh "$PIHOLE_HOST" "
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if grep -qF '${DIRECTIVE}' '${DNSMASQ_CONF}' 2>/dev/null; then
|
|
||||||
echo '==> Forwarder already configured, skipping write.'
|
|
||||||
else
|
|
||||||
echo '${DIRECTIVE}' | sudo tee '${DNSMASQ_CONF}'
|
|
||||||
echo '==> Written to ${DNSMASQ_CONF}'
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo '==> Restarting Pi-hole DNS...'
|
|
||||||
sudo pihole restartdns
|
|
||||||
|
|
||||||
echo '==> Verifying SRV record resolution via Pi-hole...'
|
|
||||||
sleep 2
|
|
||||||
dig +short _kerberos._udp.${IPA_DOMAIN} SRV @127.0.0.1 || true
|
|
||||||
"
|
|
||||||
|
|
||||||
echo "==> Done. Pi-hole now forwards ${IPA_DOMAIN} queries to ${IPA_IP}."
|
|
||||||
echo " Verify from a LAN client:"
|
|
||||||
echo " dig +short _kerberos._udp.${IPA_DOMAIN} SRV @${PIHOLE_HOST}"
|
|
||||||
@@ -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