Author SHA1 Message Date
beatzaplenty 143eebcbf3 Correct AP-isolation guess: confirmed as a 4addr forwarding gap instead
Tested live: AP isolation was already off on the TP-Link BE9300, and a
second wireless device (phone) failed identically to the laptop - rules
out isolation entirely. Real cause is the router's 4addr/WDS handling
not forwarding wireless-client broadcast/ARP traffic to pve-test's WDS
association, even though wired forwarding to it works fine. Since ARP
never resolves for wireless peers, this affects every protocol (web UI
included), not just ICMP - corrects an earlier draft of this doc that
speculated TCP might still work.
2026-07-21 21:35:08 +00:00
beatzaplenty dc4eff97fa Document AP/client-isolation gotcha for wifi clients reaching pve-test
Found live: a laptop on the same wifi network as pve-test couldn't reach
it at all (no ARP entry, not stale) despite every wired device working
fine and the mgmt firewall already allowing ICMP - the router's AP/client
isolation was blocking wireless-to-wireless traffic. Router-side fix
only; nothing on pve-test to change for this symptom.
2026-07-21 21:26:26 +00:00
beatzaplenty ba73420350 Document pve-test's wifi-primary network and add Claude node guardrails
pve-test was briefly clustered with pve1 then deliberately de-clustered
so it could move to wifi-primary networking (4addr bridge mode, bonded
with a wired LAN backup) - a change not achievable while clustered given
corosync's latency requirements. Captures that as a reproducible script
plus docs: cluster separation procedure, the wifi network design and the
live-cutover pitfalls hit along the way, and node-role/history context.

Also adds CLAUDE.md guardrails for pve1 (production) vs pve-test
(sandbox) - this repo had none before, despite scripts here being able
to make real changes to both.

Separately: both nodes' mgmt firewalls were dropping ICMP by default
(TCP 22/8006 only), which looked like an outage mid-troubleshooting even
though SSH/web UI were fine. Added an explicit ping-allow rule to the
firewall template, applied it live on both nodes, and added an audit.sh
check so it stays enforced.
2026-07-21 21:20:27 +00:00
38 changed files with 219 additions and 2675 deletions

No files matched your search

-28
View File
@@ -1,28 +0,0 @@
name: Secret Scan
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
secret-scan:
name: Scan for secrets and sensitive config
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for gitleaks git-log scan
- name: Install gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Run secret scan
run: bash scripts/check-secrets.sh
-28
View File
@@ -1,28 +0,0 @@
name: Secret Scan
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
secret-scan:
name: Scan for secrets and sensitive config
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for gitleaks git-log scan
- name: Install gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Run secret scan
run: bash scripts/check-secrets.sh
View File
File renamed without changes.
-41
View File
@@ -1,41 +0,0 @@
# Gitleaks configuration for debian-configuration repo.
# Extends the default ruleset with Pi-hole-specific secret patterns.
# https://github.com/gitleaks/gitleaks
title = "debian-configuration secret scan"
[extend]
useDefault = true
# ── Custom rules ───────────────────────────────────────────────────────────────
[[rules]]
id = "pihole-pwhash"
description = "Pi-hole password hash (pihole.toml webserver.api.pwhash)"
regex = '''pwhash\s*=\s*"[^"]{10,}"'''
tags = ["pihole", "password"]
[[rules]]
id = "pihole-totp-secret"
description = "Pi-hole 2FA TOTP secret"
regex = '''totp_secret\s*=\s*"[^"]{10,}"'''
tags = ["pihole", "2fa"]
[[rules]]
id = "pihole-app-pwhash"
description = "Pi-hole app password hash"
regex = '''app_pwhash\s*=\s*"[^"]{10,}"'''
tags = ["pihole", "password"]
# ── Allowlist ──────────────────────────────────────────────────────────────────
[allowlist]
description = "Known-safe patterns in this repo"
regexes = [
# TLS cert path reference — not the key itself
'''cert\s*=\s*"/etc/pihole/tls\.pem"''',
]
paths = [
# Example/template files are intentionally non-live
'''\.example$''',
]
+72 -36
View File
@@ -1,48 +1,84 @@
# CLAUDE.md
Guidance for Claude Code working in this repo. These instructions
OVERRIDE any default behaviour and must be followed exactly.
Guidance for Claude Code working in this repo. IMPORTANT: these
instructions OVERRIDE any default behavior and must be followed exactly
as written.
## Repo purpose
Configuration toolsets and docs for Wayne's Debian-based LAN machines.
Two sections currently exist: `proxmox/` (Proxmox VE hosts) and
`pihole/` (Pi-hole DNS/DHCP). Each has its own README.
Base configuration/hardening toolset and planning docs for Proxmox VE
hosts (`scripts/`, `config/`, `docs/`) — see `README.md` and
`docs/00-overview.md`. Scripts in this repo are meant to be run **on**
the target Proxmox host itself (as root), not orchestrated remotely.
## Safety rules (apply everywhere in this repo)
## Two Proxmox nodes: `pve1` (production) and `pve-test` (sandbox)
- **Never commit secrets.** SSH private keys, passwords, hashes, TOTP
seeds, API tokens, or wifi passphrases must not appear in committed
files. The CI pipeline (`scripts/check-secrets.sh`) enforces this on
every push; the pre-commit hook (`scripts/install-hooks.sh`) catches
it locally before it reaches the remote.
- **`pihole/pull-config.sh` auto-sanitises** sensitive fields from
`pihole.toml` on every pull. If you write config to `pihole/config/`
by any other means, run `pihole/sanitize-config.sh` on the result
before committing.
- **Scripts that SSH into live machines make real changes.** Don't run
`pihole/apply-config.sh` against a production Pi-hole, or any script
in `proxmox/scripts/` against `pve1`, without an explicit same-session
go-ahead from the operator. The section-specific CLAUDE.md files
spell out the per-host guardrails in detail.
Two SSH-reachable Proxmox nodes exist on the LAN. They are **not
interchangeable** — see `docs/05-node-roles.md` for full background on
what each one is and why.
## Section-specific guidance
### `pve1` (production — off-limits to Claude by default)
- **Proxmox:** see `proxmox/CLAUDE.md` — covers `pve1` (production,
off-limits by default) vs. `pve-test` (sandbox, Claude's default
target), per-host authorisation scope, and network-surgery hazards.
- **Pi-hole:** `pihole/` has no live-host guardrails beyond the secret
rules above. `apply-config.sh` is the only script that touches a live
host; treat its `<dest-host>` argument as production unless you're
explicitly testing on a throwaway instance.
A real, live Proxmox node hosting production VMs/containers (see
`docs/05-node-roles.md` for the current guest list) — not a sandbox, and
not Claude's to touch by default.
## Adding a new machine type
- **Off-limits at all times unless the operator has given explicit,
same-session instructions to act on this specific host.** That
authorization is scoped to the task it was given for — don't carry it
forward to unrelated later work in the same conversation, and never
assume it from a previous session.
- **Read-only for existing state is always fine, authorization or not.**
SSH in (or use `pvesm`, `qm list`, `pct list`, `qm config`, `pct
config`, the Proxmox API, etc.) to inspect config, storage, and any
existing VM/container freely.
- **Never** modify, stop, restart, delete, reconfigure, or create
anything on this node (`qm set`, `pct set`, `qm destroy`, `pct
destroy`, `qm stop`, `pct stop`, `qm create`, `pct create`, snapshot
operations, storage changes, running any script in this repo against
it, etc.) without that explicit go-ahead. Use `pve-test` for anything
exploratory instead.
- If a guest on `pve1` is HA-managed, be aware of the self-fence hazard
described in `docs/05-node-roles.md`'s cluster-teardown section before
doing anything that could cost the node quorum.
Create a new top-level directory (e.g. `nginx/`, `wireguard/`) with:
- `README.md` — purpose, quick-start, and current status
- `CLAUDE.md` — host-specific guardrails (which hosts are production,
what requires explicit authorisation, what must never be committed)
- `scripts/` and/or `config/` as needed
### `pve-test` (sandbox — Claude's default target)
Update the root `README.md` layout table and this file's
"Section-specific guidance" list when you do.
A separate node set aside for testing — safe to create, interrogate, and
destroy scratch VMs/containers on without asking first.
- **Test VMs/containers are allowed, but must be torn down.** Anything
created this way must be destroyed again in the same session, before
ending the task. Use an obviously-scratch VMID/name.
- **Node-level config is still not yours to change by default.**
Creating/destroying your own scratch guests is fine; Proxmox host
config, storage pools, and networking on `pve-test` itself need the
operator's explicit go-ahead too, same as on `pve1` — the "sandbox"
status covers guest-level experimentation, not the host's own
identity. (`pve-test`'s current network config, `docs/06-pve-test-wifi-network.md`,
was applied under exactly that kind of explicit, same-session
authorization — it's not a standing invitation to keep changing it
further without asking again.)
- **Never re-cluster `pve-test` with `pve1`** without the operator
explicitly asking for it and being aware of the wifi/corosync
incompatibility in `docs/06-pve-test-wifi-network.md` — the two were
deliberately de-clustered for this reason once already.
## Safety rules
- Any script in `scripts/` that isn't `audit.sh` (read-only) makes real
changes when run for real. Don't run one against `pve1`, or against
`pve-test`'s node-level config, without the same-session go-ahead
described above. Running a script *against pve-test's own guests* — a
scratch VM/CT you created for this task — doesn't need separate
permission.
- Do not commit secrets: SSH private keys, wifi passphrases, sops/age
keys, or PVE credentials. `scripts/setup-wifi-bond-network.sh` takes
the SSID/passphrase via environment variables for exactly this reason
— never hardcode them into the script or a committed config file.
- Live changes to a node's own management network (the interface/bridge
carrying the SSH session you're using) can strand the box — see the
"what went wrong once" section in `docs/06-pve-test-wifi-network.md`
before touching `pve-test`'s networking again. Prefer applying via
`ifreload -a` over raw `ip link` surgery, and arm an auto-revert
watchdog first when acting without someone physically at the console.
+48 -39
View File
@@ -1,46 +1,55 @@
# debian-configuration
# Proxmox Configuration
Configuration, hardening toolsets, and operational docs for Wayne's
Debian-based LAN machines. Each subdirectory covers a different host or
service type.
Base configuration and hardening toolset for Proxmox VE hosts, plus planning
docs for eventually growing this into a 3-node HA/Ceph cluster. See
`docs/00-overview.md` for the staging: **Stage 1** (base config/hardening,
applies to any host — active) vs. **Stage 2** (multi-node HA/Ceph — future,
deferred).
## Layout
## Goals
- Stage 1: a reusable, idempotent base-hardening toolset (`scripts/`) that
can be run against any new Proxmox host — repo/updates, SSH, firewall, PVE
user/access hardening — verified with `scripts/audit.sh`.
- Stage 2 (future): 3-node cluster, quorum via corosync, HA-managed VMs
backed by Ceph. Needs dedicated hardware node 1 (`pve1`, an ASUS PN53 mini
PC) doesn't have — see `docs/01-hardware-node1.md`.
See `CLAUDE.md` for the guardrails Claude Code follows when working
against these hosts (`pve1` is production and off-limits by default;
`pve-test` is the sandbox).
## Repo layout
- `docs/` — planning docs: hardware layout, storage migration, networking,
security hardening, node roles. Read `docs/00-overview.md` first, then
`docs/05-node-roles.md` for what `pve1`/`pve-test` actually are.
- `scripts/` — scripts to apply configuration on a node (SSH hardening, repo
switch, firewall, updates, wifi/bond networking, etc). Idempotent, safe
to re-run. `scripts/bootstrap.sh` runs the full Stage 1 sequence end to
end; `scripts/audit.sh` verifies it (read-only).
`scripts/setup-wifi-bond-network.sh` reproduces `pve-test`'s wifi
network (see `docs/06-pve-test-wifi-network.md`). `scripts/lib/` holds
shared helpers (`common.sh`) sourced by the other scripts.
- `config/` — reference config files/snippets to drop onto a node (firewall
rules, sshd config, etc.).
## Quick start (Stage 1, on a fresh node)
```
proxmox/ Proxmox VE hosts (pve1 production, pve-test sandbox)
pihole/ Pi-hole DNS/DHCP (config snapshots, pull/apply scripts)
scripts/ Repo-wide scripts (secret scanning, git hook installer)
MGMT_CIDR=192.168.2.0/24 ./scripts/bootstrap.sh
./scripts/create-admin-user.sh <username>
# then enable 2FA for that user + root@pam via the web UI
./scripts/audit.sh
```
## Sections
## Status
### `proxmox/`
Base configuration and hardening toolset for Proxmox VE hosts. See
`proxmox/README.md` for goals, quick-start, and current status.
### `pihole/`
Pi-hole v6 configuration management. Stores a sanitised snapshot of the
live config and provides scripts to pull from or push to a running
instance. See `pihole/README.md` for usage.
## Secret scanning
All commits are scanned for secrets by a CI pipeline that runs on both
GitHub Actions and Gitea Actions. The same scan can be run locally:
```bash
# One-time setup — installs a pre-commit git hook
bash scripts/install-hooks.sh
# Manual run against the full git history
bash scripts/check-secrets.sh
# Requires gitleaks on PATH; falls back to Docker if available
# https://github.com/gitleaks/gitleaks#installing
```
`pihole/pull-config.sh` automatically redacts sensitive fields
(`pwhash`, `totp_secret`, `app_pwhash`) from `pihole.toml` before
writing it to disk, so the repo stays clean by default.
`pve1` built (ASUS PN53 mini PC, ZFS mirror boot+VM storage, single
2.5GbE NIC) and already running production VMs/CTs. Stage 1 base
hardening applied and verified (`scripts/audit.sh` all green): enterprise
repos removed, SSH key-only + fail2ban, unattended security upgrades,
PVE firewall (mgmt-only), subscription nag disabled, named admin user
(`wayne@pve`) created. Remaining manual step: enable 2FA/TOTP for
`wayne@pve` and `root@pam` via the web UI. Stage 2 (cluster/Ceph) not
started — needs nodes 2/3 on hardware that can actually support it.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -16,31 +16,24 @@ Then run `scripts/audit.sh` to verify. Order matters (matches
| # | Item | Script | Manual step required? |
|---|------|--------|------------------------|
| 1 | Remove enterprise repos, switch to no-subscription | `switch-to-no-subscription-repo.sh` | no |
| 2 | Linux admin user with SSH key + sudo access | `setup-linux-admin-user.sh <user> <pubkey>` | yes — choose a username and provide your SSH public key. Run this **before** SSH hardening or pass `ADMIN_USER`/`ADMIN_SSH_KEY` to `bootstrap.sh` so it runs automatically at the right point |
| 3 | Passwordless sudo for pvesh/qm/pct | `setup-admin-sudo.sh <username>` | yes — same username as step 2 |
| 4 | SSH: key-only root login + fail2ban | `harden-ssh.sh` | no — step 2 ensures authorized_keys are in place first |
| 5 | Unattended security upgrades, no auto-reboot | `setup-unattended-upgrades.sh` | no |
| 6 | PVE firewall, default-deny, mgmt-only SSH/8006 | `deploy-firewall.sh` | needs `MGMT_CIDR` set |
| 7 | Disable subscription nag (cosmetic) | `disable-subscription-nag.sh` | no |
| 8 | Named PVE admin user, Administrator role | `create-admin-user.sh <username>` | yes — pick the username, change the generated password on first login |
| 9 | 2FA/TOTP on that user and `root@pam` | — | yes — web UI only: Datacenter → Permissions → Two Factor, or user menu → TFA |
| 10 | Verify everything above | `audit.sh` | no |
| 2 | SSH: key-only root login + fail2ban | `harden-ssh.sh` | no (requires an `authorized_keys` already in place — script warns if missing) |
| 3 | Unattended security upgrades, no auto-reboot | `setup-unattended-upgrades.sh` | no |
| 4 | PVE firewall, default-deny, mgmt-only SSH/8006 | `deploy-firewall.sh` | needs `MGMT_CIDR` set |
| 5 | Disable subscription nag (cosmetic) | `disable-subscription-nag.sh` | no |
| 6 | Named PVE admin user, Administrator role | `create-admin-user.sh <username>` | yes — pick the username, change the generated password on first login |
| 7 | 2FA/TOTP on that user and `root@pam` | — | yes — web UI only: Datacenter → Permissions → Two Factor, or user menu → TFA |
| 8 | Verify everything above | `audit.sh` | no |
## Linux/SSH layer
- A named Linux system user (created by `setup-linux-admin-user.sh`) with an
SSH authorized key and `sudo` group membership is the primary SSH login
account. Root SSH is locked to key-only after `harden-ssh.sh` runs; the
Linux admin user is how you get shell access day-to-day without using root.
- `setup-admin-sudo.sh` adds a narrower, password-free sudoers rule for the
Proxmox management tools (`pvesh`, `qm`, `pct`) specifically — needed for
non-interactive automation scripts that SSH in and run these tools without a
TTY.
- `PermitRootLogin prohibit-password` in `sshd_config` — root can only
log in via SSH key, never password. Kills most brute-force attempts.
- fail2ban jail for SSH on top of that.
- Restrict SSH to the management VLAN/trusted IPs via the Proxmox
firewall (see `03-networking.md`) rather than exposing broadly.
- A separate Linux sudo user isn't strictly required for day-to-day PVE
admin (the PVE permission system below governs that), but worth adding
if multiple people SSH into the box directly, for accountability.
## PVE/web layer (the one that actually matters day-to-day)
File renamed without changes.
@@ -181,6 +181,45 @@ a real outage**: SSH (`ssh root@pve-test.sweet.home`) → web UI via `curl
only then worry about `ping` specifically, and check
`/etc/pve/firewall/cluster.fw` before blaming the network.
## Confirmed limitation: no wireless client on the same AP can reach pve-test
**This is not an AP-isolation toggle** — tested and ruled out (router:
TP-Link BE9300, AP Isolation confirmed unchecked). Two independent
wireless devices (a laptop and a phone) on the same SSID pve-test uses
(`nbn-fttp-net-5G`) were both completely unable to reach it — not
intermittent, not slow, no ARP entry ever resolves (`arp -a` on Windows /
`ip neigh` on Linux/Mac shows nothing at all, not a stale entry). Every
wired device tested (this repo's own Claude session, `pve1`) reaches it
fine, every time.
Root cause: pve-test's wifi connection uses 4-address (WDS) framing to
make bridging possible at all (see above) — that's what lets the AP
forward frames from arbitrary MACs (VM traffic, the bridge's own
identity) instead of only the wifi card's own hardware MAC. This
router's firmware evidently handles wired↔pve-test forwarding for that
4addr peer correctly, but doesn't correctly forward *wireless-client*
broadcast/ARP traffic to it — an asymmetric gap in the AP's own 4addr
handling, not a policy setting. Likely not fixable short of a firmware
update from TP-Link (if one ever improves 4addr/WDS handling), possibly
not fixable on this hardware at all.
**Practical implication**: anything on `vmbr0` — pve-test itself, or any
VM/CT created on it — is unreachable from a wireless client on this AP,
full stop, for *any* protocol, not just ICMP. ARP resolution is a
prerequisite for sending any IP packet at all (TCP included); since ARP
itself never resolves for a wireless peer here, the web UI (8006) fails
identically to ping, not just ping — confirmed live, not theoretical.
This includes VMs on pve-test too, since they ride the same bridge/wifi
uplink as the host. **Wired access is the only reliable path to
pve-test and anything running on it** — including reaching a VM's
console through the Proxmox web UI, which still requires the accessing
device to reach pve-test's own IP on port 8006 first, so it inherits the
same requirement.
Don't waste time on ARP flushes, firewall rules, or bonding config for
this specific symptom (no ARP entry from a wireless peer, wired fine) —
none of those are the cause.
## Known limitation history
`nic0` (the bond's backup slave) initially had no cable physically
-67
View File
@@ -1,67 +0,0 @@
# pihole
Pi-hole v6 configuration management for `pihole.sweet.home`.
## What's here
```
config/
pihole.toml Pi-hole v6 main config (sanitised snapshot)
dnsmasq.d/
99-ipxe-chainload.conf Custom DHCP rules for EFI/BIOS iPXE PXE boot
pull-config.sh Pull live config from a Pi-hole to a local dir
apply-config.sh Apply a local config dir to a Pi-hole instance
sanitize-config.sh Redact sensitive fields from pihole.toml in-place
```
## Usage
### Snapshot the live config
```bash
# Pulls pihole.toml + custom dnsmasq.d/ drop-ins, auto-redacts sensitive fields
./pull-config.sh root@pihole ./config
```
### Apply config to a new Pi-hole
```bash
# Destination must have Pi-hole v6 already installed
./apply-config.sh ./config root@pihole-new
```
Both scripts take `<source> <destination>` as positional arguments.
Source/destination is an SSH target (e.g. `root@pihole`, `root@192.168.2.253`)
for the live side and a local directory path for the config side.
### Restore blocklists after applying
`apply-config.sh` transfers config only — not the gravity database.
Run this on the destination after applying to rebuild blocklists:
```bash
ssh root@pihole-new "pihole updateGravity"
```
## Notable config
### `dnsmasq.d/99-ipxe-chainload.conf`
Enables architecture-aware PXE boot via DHCP:
| Client | Vendor class | Boot file served |
|---|---|---|
| EFI iPXE already running | arch 7 + option 175 | `http://.../boot.ipxe` |
| BIOS iPXE already running | arch 0 + option 175 | `http://.../boot.ipxe` |
| EFI, no iPXE yet | `PXEClient:Arch:00007` | `ipxe.efi` via TFTP |
| BIOS/legacy, no iPXE yet | `PXEClient:Arch:00000` | `undionly.kpxe` via TFTP |
Proxmox EFI VMs also require a **VirtIO RNG device** (gives OVMF
enough entropy to complete PXE negotiation) and **Secure Boot disabled**
(`pre-enrolled-keys=0` on the EFI disk, or disabled in UEFI setup).
## Secrets
`pihole.toml` is stored with `pwhash`, `totp_secret`, and `app_pwhash`
redacted to empty strings. `pull-config.sh` does this automatically.
To redact manually: `./sanitize-config.sh config/pihole.toml`.
-60
View File
@@ -1,60 +0,0 @@
#!/usr/bin/env bash
# Apply a Pi-hole configuration directory to a destination Pi-hole instance.
#
# Usage: apply-config.sh <source-dir> <dest-host>
# source-dir Local directory containing config files (as written by pull-config.sh)
# dest-host SSH-reachable hostname or IP of the destination Pi-hole
#
# Example:
# ./apply-config.sh ./config root@pihole-new
# ./apply-config.sh /backup/pihole-20260723 root@192.168.2.101
#
# The destination Pi-hole must already have Pi-hole v6 installed.
# pihole-FTL is restarted at the end to apply the new config.
set -euo pipefail
usage() {
echo "Usage: $(basename "$0") <source-dir> <dest-host>" >&2
echo " source-dir Local directory with config files (from pull-config.sh)" >&2
echo " dest-host SSH target for the destination Pi-hole (e.g. root@pihole)" >&2
exit 1
}
[[ $# -eq 2 ]] || usage
SOURCE="$1"
DEST="$2"
[[ -d "${SOURCE}" ]] || { echo "Error: source directory '${SOURCE}' not found" >&2; exit 1; }
[[ -f "${SOURCE}/pihole.toml" ]] || { echo "Error: '${SOURCE}/pihole.toml' not found — is this a valid config dir?" >&2; exit 1; }
echo "Applying Pi-hole config from ${SOURCE} → ${DEST}"
# ── Verify destination is running Pi-hole ──────────────────────────────────────
ssh "${DEST}" "command -v pihole-FTL >/dev/null 2>&1 || { echo 'pihole-FTL not found on destination'; exit 1; }"
# ── pihole.toml ────────────────────────────────────────────────────────────────
echo " pihole.toml"
ssh "${DEST}" "cp /etc/pihole/pihole.toml /etc/pihole/pihole.toml.pre-apply 2>/dev/null || true"
scp "${SOURCE}/pihole.toml" "${DEST}:/etc/pihole/pihole.toml"
ssh "${DEST}" "chown pihole:pihole /etc/pihole/pihole.toml; chmod 640 /etc/pihole/pihole.toml"
# ── custom dnsmasq drop-ins ────────────────────────────────────────────────────
if [[ -d "${SOURCE}/dnsmasq.d" ]] && [[ -n "$(ls "${SOURCE}/dnsmasq.d/"*.conf 2>/dev/null)" ]]; then
echo " dnsmasq.d/ (custom drop-ins)"
for f in "${SOURCE}/dnsmasq.d/"*.conf; do
name="$(basename "$f")"
echo " ${name}"
scp "${f}" "${DEST}:/etc/dnsmasq.d/${name}"
done
fi
# ── Restart pihole-FTL ─────────────────────────────────────────────────────────
echo " Restarting pihole-FTL"
ssh "${DEST}" "systemctl restart pihole-FTL"
sleep 2
ssh "${DEST}" "systemctl is-active pihole-FTL"
echo "Done. Config applied to ${DEST}."
echo "Note: gravity (blocklists) is not transferred — run 'pihole updateGravity' on ${DEST} to rebuild."
@@ -1,20 +0,0 @@
# Detect iPXE clients (already running iPXE).
dhcp-match=set:ipxe,175
dhcp-userclass=set:ipxe,iPXE
# Detect UEFI x86_64 clients by client architecture.
dhcp-match=set:efi64,option:client-arch,7
dhcp-match=set:efi64,option:client-arch,9
# Boot file selection — more positive tags = higher priority.
# EFI iPXE (2 tags): already running iPXE on EFI, chain to HTTP menu.
dhcp-boot=tag:ipxe,tag:efi64,http://192.168.2.247/boot.ipxe
# BIOS iPXE (1 tag): already running iPXE, chain to HTTP menu.
dhcp-boot=tag:ipxe,http://192.168.2.247/boot.ipxe
# EFI non-iPXE (1 tag): send the EFI iPXE binary.
dhcp-boot=tag:efi64,ipxe.efi,,192.168.2.247
# BIOS/legacy fallback (0 tags): send the BIOS iPXE binary.
dhcp-boot=undionly.kpxe,,192.168.2.247
File diff suppressed because it is too large. Load diff
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Pull Pi-hole configuration from a running instance to a local directory.
# Sensitive fields (password hashes, TOTP secrets) are redacted automatically.
#
# Usage: pull-config.sh <source-host> <dest-dir>
# source-host SSH-reachable hostname or IP of the source Pi-hole
# dest-dir Local directory to write config into (created if absent)
#
# Example:
# ./pull-config.sh root@pihole ./config
# ./pull-config.sh root@192.168.2.253 /backup/pihole-$(date +%Y%m%d)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
usage() {
echo "Usage: $(basename "$0") <source-host> <dest-dir>" >&2
echo " source-host SSH target for the source Pi-hole (e.g. root@pihole)" >&2
echo " dest-dir Local directory to write config files into" >&2
exit 1
}
[[ $# -eq 2 ]] || usage
SOURCE="$1"
DEST="$2"
echo "Pulling Pi-hole config from ${SOURCE} → ${DEST}"
mkdir -p "${DEST}/dnsmasq.d"
# ── pihole.toml ────────────────────────────────────────────────────────────────
echo " pihole.toml"
ssh "${SOURCE}" "cat /etc/pihole/pihole.toml" > "${DEST}/pihole.toml"
"${SCRIPT_DIR}/sanitize-config.sh" "${DEST}/pihole.toml"
# ── custom dnsmasq drop-ins ────────────────────────────────────────────────────
# Pi-hole manages its own generated config; we only capture user-added files.
echo " dnsmasq.d/ (custom drop-ins)"
ssh "${SOURCE}" "ls /etc/dnsmasq.d/*.conf 2>/dev/null || true" | while read -r f; do
name="$(basename "$f")"
echo " ${name}"
ssh "${SOURCE}" "cat '${f}'" > "${DEST}/dnsmasq.d/${name}"
done
# ── DHCP static leases ─────────────────────────────────────────────────────────
echo " dhcp.leases"
ssh "${SOURCE}" "cat /etc/pihole/dhcp.leases 2>/dev/null || true" > "${DEST}/dhcp.leases"
echo "Done. Config written to ${DEST}/"
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Redact sensitive fields from a Pi-hole pihole.toml before committing.
# Called automatically by pull-config.sh; can also be run manually.
#
# Usage: sanitize-config.sh <pihole.toml>
# Edits the file in-place, replacing sensitive field values with "".
set -euo pipefail
usage() {
echo "Usage: $(basename "$0") <pihole.toml>" >&2
exit 1
}
[[ $# -eq 1 ]] || usage
FILE="$1"
[[ -f "$FILE" ]] || { echo "Error: file not found: $FILE" >&2; exit 1; }
REDACTED=0
redact_field() {
local field="$1"
# Match lines like: pwhash = "some-value" and blank the value
if grep -qE "^\s+${field}\s*=\s*\"[^\"]{1,}\"" "$FILE"; then
sed -i -E "s|^(\s+${field}\s*=\s*)\"[^\"]*\"|\1\"\"|" "$FILE"
echo " redacted: ${field}"
REDACTED=$((REDACTED + 1))
fi
}
echo "Sanitizing $(basename "$FILE")..."
redact_field "pwhash"
redact_field "app_pwhash"
redact_field "totp_secret"
if [[ $REDACTED -eq 0 ]]; then
echo " (nothing to redact)"
else
echo " ${REDACTED} field(s) redacted."
fi
-84
View File
@@ -1,84 +0,0 @@
# CLAUDE.md
Guidance for Claude Code working in this repo. IMPORTANT: these
instructions OVERRIDE any default behavior and must be followed exactly
as written.
## Repo purpose
Base configuration/hardening toolset and planning docs for Proxmox VE
hosts (`scripts/`, `config/`, `docs/`) — see `README.md` and
`docs/00-overview.md`. Scripts in this repo are meant to be run **on**
the target Proxmox host itself (as root), not orchestrated remotely.
## Two Proxmox nodes: `pve1` (production) and `pve-test` (sandbox)
Two SSH-reachable Proxmox nodes exist on the LAN. They are **not
interchangeable** — see `docs/05-node-roles.md` for full background on
what each one is and why.
### `pve1` (production — off-limits to Claude by default)
A real, live Proxmox node hosting production VMs/containers (see
`docs/05-node-roles.md` for the current guest list) — not a sandbox, and
not Claude's to touch by default.
- **Off-limits at all times unless the operator has given explicit,
same-session instructions to act on this specific host.** That
authorization is scoped to the task it was given for — don't carry it
forward to unrelated later work in the same conversation, and never
assume it from a previous session.
- **Read-only for existing state is always fine, authorization or not.**
SSH in (or use `pvesm`, `qm list`, `pct list`, `qm config`, `pct
config`, the Proxmox API, etc.) to inspect config, storage, and any
existing VM/container freely.
- **Never** modify, stop, restart, delete, reconfigure, or create
anything on this node (`qm set`, `pct set`, `qm destroy`, `pct
destroy`, `qm stop`, `pct stop`, `qm create`, `pct create`, snapshot
operations, storage changes, running any script in this repo against
it, etc.) without that explicit go-ahead. Use `pve-test` for anything
exploratory instead.
- If a guest on `pve1` is HA-managed, be aware of the self-fence hazard
described in `docs/05-node-roles.md`'s cluster-teardown section before
doing anything that could cost the node quorum.
### `pve-test` (sandbox — Claude's default target)
A separate node set aside for testing — safe to create, interrogate, and
destroy scratch VMs/containers on without asking first.
- **Test VMs/containers are allowed, but must be torn down.** Anything
created this way must be destroyed again in the same session, before
ending the task. Use an obviously-scratch VMID/name.
- **Node-level config is still not yours to change by default.**
Creating/destroying your own scratch guests is fine; Proxmox host
config, storage pools, and networking on `pve-test` itself need the
operator's explicit go-ahead too, same as on `pve1` — the "sandbox"
status covers guest-level experimentation, not the host's own
identity. (`pve-test`'s current network config, `docs/06-pve-test-wifi-network.md`,
was applied under exactly that kind of explicit, same-session
authorization — it's not a standing invitation to keep changing it
further without asking again.)
- **Never re-cluster `pve-test` with `pve1`** without the operator
explicitly asking for it and being aware of the wifi/corosync
incompatibility in `docs/06-pve-test-wifi-network.md` — the two were
deliberately de-clustered for this reason once already.
## Safety rules
- Any script in `scripts/` that isn't `audit.sh` (read-only) makes real
changes when run for real. Don't run one against `pve1`, or against
`pve-test`'s node-level config, without the same-session go-ahead
described above. Running a script *against pve-test's own guests* — a
scratch VM/CT you created for this task — doesn't need separate
permission.
- Do not commit secrets: SSH private keys, wifi passphrases, sops/age
keys, or PVE credentials. `scripts/setup-wifi-bond-network.sh` takes
the SSID/passphrase via environment variables for exactly this reason
— never hardcode them into the script or a committed config file.
- Live changes to a node's own management network (the interface/bridge
carrying the SSH session you're using) can strand the box — see the
"what went wrong once" section in `docs/06-pve-test-wifi-network.md`
before touching `pve-test`'s networking again. Prefer applying via
`ifreload -a` over raw `ip link` surgery, and arm an auto-revert
watchdog first when acting without someone physically at the console.
-57
View File
@@ -1,57 +0,0 @@
> Part of the [debian-configuration](../README.md) repo — see the root README for repo-wide layout and secret-scanning setup.
# Proxmox Configuration
Base configuration and hardening toolset for Proxmox VE hosts, plus planning
docs for eventually growing this into a 3-node HA/Ceph cluster. See
`docs/00-overview.md` for the staging: **Stage 1** (base config/hardening,
applies to any host — active) vs. **Stage 2** (multi-node HA/Ceph — future,
deferred).
## Goals
- Stage 1: a reusable, idempotent base-hardening toolset (`scripts/`) that
can be run against any new Proxmox host — repo/updates, SSH, firewall, PVE
user/access hardening — verified with `scripts/audit.sh`.
- Stage 2 (future): 3-node cluster, quorum via corosync, HA-managed VMs
backed by Ceph. Needs dedicated hardware node 1 (`pve1`, an ASUS PN53 mini
PC) doesn't have — see `docs/01-hardware-node1.md`.
See `CLAUDE.md` for the guardrails Claude Code follows when working
against these hosts (`pve1` is production and off-limits by default;
`pve-test` is the sandbox).
## Repo layout
- `docs/` — planning docs: hardware layout, storage migration, networking,
security hardening, node roles. Read `docs/00-overview.md` first, then
`docs/05-node-roles.md` for what `pve1`/`pve-test` actually are.
- `scripts/` — scripts to apply configuration on a node (SSH hardening, repo
switch, firewall, updates, wifi/bond networking, etc). Idempotent, safe
to re-run. `scripts/bootstrap.sh` runs the full Stage 1 sequence end to
end; `scripts/audit.sh` verifies it (read-only).
`scripts/setup-wifi-bond-network.sh` reproduces `pve-test`'s wifi
network (see `docs/06-pve-test-wifi-network.md`). `scripts/lib/` holds
shared helpers (`common.sh`) sourced by the other scripts.
- `config/` — reference config files/snippets to drop onto a node (firewall
rules, sshd config, etc.).
## Quick start (Stage 1, on a fresh node)
```
MGMT_CIDR=192.168.2.0/24 ./scripts/bootstrap.sh
./scripts/create-admin-user.sh <username>
# then enable 2FA for that user + root@pam via the web UI
./scripts/audit.sh
```
## Status
`pve1` built (ASUS PN53 mini PC, ZFS mirror boot+VM storage, single
2.5GbE NIC) and already running production VMs/CTs. Stage 1 base
hardening applied and verified (`scripts/audit.sh` all green): enterprise
repos removed, SSH key-only + fail2ban, unattended security upgrades,
PVE firewall (mgmt-only), subscription nag disabled, named admin user
(`wayne@pve`) created. Remaining manual step: enable 2FA/TOTP for
`wayne@pve` and `root@pam` via the web UI. Stage 2 (cluster/Ceph) not
started — needs nodes 2/3 on hardware that can actually support it.
-67
View File
@@ -1,67 +0,0 @@
#!/bin/bash
# Stage 1 base config + hardening, end to end, for a single fresh PVE host.
# Runs the individual scripts in order. Idempotent - safe to re-run.
#
# If ADMIN_USER and ADMIN_SSH_KEY are set, a Linux system user is created
# with SSH key access and sudo before SSH hardening runs - so key-based
# login is in place before password auth is disabled. If they are not set,
# a reminder is printed at the end to run setup-linux-admin-user.sh manually
# (but do this BEFORE disconnecting, since password auth will be disabled).
#
# Usage:
# MGMT_CIDR=192.168.2.0/24 ./bootstrap.sh
# MGMT_CIDR=192.168.2.0/24 ADMIN_USER=wayne ADMIN_SSH_KEY="ssh-ed25519 ..." ./bootstrap.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 [ -z "${MGMT_CIDR:-}" ]; then
echo "MGMT_CIDR is not set. Example: MGMT_CIDR=192.168.2.0/24 $0" >&2
exit 1
fi
STEP=0
next_step() { STEP=$((STEP + 1)); echo; echo "=== ${STEP}: $* ==="; }
next_step "remove enterprise repos, switch to no-subscription"
"${SCRIPT_DIR}/switch-to-no-subscription-repo.sh"
# Create the Linux admin user before SSH hardening so that authorized_keys
# is in place before password auth is disabled.
if [ -n "${ADMIN_USER:-}" ] && [ -n "${ADMIN_SSH_KEY:-}" ]; then
next_step "Linux admin user '${ADMIN_USER}' + SSH key + sudo group"
"${SCRIPT_DIR}/setup-linux-admin-user.sh" "$ADMIN_USER" "$ADMIN_SSH_KEY"
next_step "passwordless sudo for pvesh/qm/pct (${ADMIN_USER})"
"${SCRIPT_DIR}/setup-admin-sudo.sh" "$ADMIN_USER"
else
echo
echo "WARNING: ADMIN_USER / ADMIN_SSH_KEY not set -- skipping Linux user setup."
echo " Run setup-linux-admin-user.sh and setup-admin-sudo.sh BEFORE disconnecting"
echo " from this session, since the next step disables password authentication."
fi
next_step "SSH hardening (key-only root login + fail2ban)"
"${SCRIPT_DIR}/harden-ssh.sh"
next_step "unattended security upgrades"
"${SCRIPT_DIR}/setup-unattended-upgrades.sh"
next_step "PVE firewall (mgmt-only SSH/8006)"
MGMT_CIDR="$MGMT_CIDR" "${SCRIPT_DIR}/deploy-firewall.sh"
next_step "disable subscription nag (cosmetic)"
"${SCRIPT_DIR}/disable-subscription-nag.sh"
echo
echo "=== Base hardening applied. Remaining manual/deliberate steps: ==="
if [ -z "${ADMIN_USER:-}" ]; then
echo " - ${SCRIPT_DIR}/setup-linux-admin-user.sh <username> <ssh-pubkey>"
echo " - ${SCRIPT_DIR}/setup-admin-sudo.sh <username> (NOPASSWD for pvesh/qm/pct)"
fi
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 " - ${SCRIPT_DIR}/audit.sh (verify everything above)"
-61
View File
@@ -1,61 +0,0 @@
#!/bin/bash
# Grant a named admin user passwordless sudo for Proxmox management tools
# (pvesh, qm, pct) and the Nix package manager so that scripts in the
# nixos flake repo can run these over non-interactive SSH without a TTY.
#
# Nix is included because single-user Nix installations (common on PVE
# hosts bootstrapped via codex-setup.sh) are owned by root; non-root
# users can't touch the Nix store lock without sudo.
#
# Idempotent - safe to re-run (rewrites if paths have changed). Run as
# root on the PVE host.
#
# Usage: ./setup-admin-sudo.sh <username>
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="${1:-}"
if [ -z "$USERNAME" ]; then
echo "Usage: $0 <username>" >&2
exit 1
fi
# Resolve actual binary paths at script time -- they differ across Proxmox
# versions (pvesh moved from /usr/sbin to /usr/bin in PVE 8.x) and the
# sudoers rule must match the real path or sudo will fall back to
# prompting for a password.
resolve_bin() {
command -v "$1" 2>/dev/null || { echo "ERROR: $1 not found on PATH" >&2; exit 1; }
}
PVESH="$(resolve_bin pvesh)"
QM="$(resolve_bin qm)"
PCT="$(resolve_bin pct)"
# Nix installs to a fixed path regardless of which user bootstrapped it.
NIX_BIN="/nix/var/nix/profiles/default/bin/nix"
if [ ! -x "$NIX_BIN" ]; then
echo "WARNING: $NIX_BIN not found -- Nix may not be installed yet." >&2
echo " Re-run this script after running codex-setup.sh on the node." >&2
NIX_BIN=""
fi
SUDOERS_FILE="/etc/sudoers.d/${USERNAME}-proxmox"
NIX_ENTRY="${NIX_BIN:+, ${NIX_BIN}}"
CONTENT="${USERNAME} ALL=(root) NOPASSWD: ${PVESH}, ${QM}, ${PCT}${NIX_ENTRY}"
write_if_changed "$SUDOERS_FILE" "$CONTENT"
# visudo -c validates the file we just wrote before we walk away.
if visudo -c -f "$SUDOERS_FILE" >/dev/null 2>&1; then
chmod 0440 "$SUDOERS_FILE"
echo "Sudoers rule for ${USERNAME} is valid and in place."
echo " ${CONTENT}"
else
echo "ERROR: sudoers validation failed -- removing bad file." >&2
rm -f "$SUDOERS_FILE"
exit 1
fi
-82
View File
@@ -1,82 +0,0 @@
#!/bin/bash
# Create a Linux system user for SSH access and sudo, and install their
# authorized SSH public key. Run this before harden-ssh.sh so that
# key-based access is in place before password authentication is disabled.
#
# Idempotent - safe to re-run. Run as root on the PVE host.
#
# Usage:
# ./setup-linux-admin-user.sh <username> <ssh-public-key>
# ./setup-linux-admin-user.sh <username> --key-file <path-to-.pub>
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="${1:-}"
if [ -z "$USERNAME" ]; then
echo "Usage: $0 <username> <ssh-public-key>" >&2
echo " $0 <username> --key-file <path-to-.pub>" >&2
exit 1
fi
shift
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 "ERROR: an SSH public key is required (key string or --key-file <path>)." >&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
# --- create Linux user if missing ---
if id "$USERNAME" >/dev/null 2>&1; then
echo "User '${USERNAME}' already exists - skipping useradd."
else
useradd --create-home --shell /bin/bash "$USERNAME"
echo "Created Linux user '${USERNAME}'."
fi
# --- sudo group membership ---
if id -nG "$USERNAME" | grep -qw sudo; then
echo "User '${USERNAME}' is already in the sudo group."
else
usermod --append --groups sudo "$USERNAME"
echo "Added '${USERNAME}' to the sudo group."
fi
# --- install authorized SSH key ---
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"
touch "$AUTH_FILE"
chmod 600 "$AUTH_FILE"
chown -R "${USERNAME}:${USERNAME}" "$SSH_DIR"
if grep -qF "$SSH_KEY" "$AUTH_FILE" 2>/dev/null; then
echo "SSH key is already present in ${AUTH_FILE}."
else
echo "$SSH_KEY" >> "$AUTH_FILE"
echo "Installed SSH key in ${AUTH_FILE}."
fi
echo
echo "Linux user '${USERNAME}' is ready for SSH key-based login with sudo access."
echo "Next: run setup-admin-sudo.sh ${USERNAME} to grant NOPASSWD for pvesh/qm/pct."
+5 -33
View File
@@ -58,6 +58,11 @@ if [ -f /etc/pve/firewall/cluster.fw ] && grep -qi '^policy_in:\s*DROP' /etc/pve
else
audit_fail "cluster.fw missing or does not default-deny inbound"
fi
if [ -f /etc/pve/firewall/cluster.fw ] && grep -qi 'icmp-type echo-request' /etc/pve/firewall/cluster.fw 2>/dev/null; then
audit_pass "cluster.fw allows ICMP echo-request from mgmt (ping works)"
else
audit_fail "cluster.fw does not allow ping from mgmt - see docs/04-security-hardening.md firewall section"
fi
# --- unattended-upgrades ---
if dpkg -s unattended-upgrades >/dev/null 2>&1 && systemctl is-enabled --quiet unattended-upgrades 2>/dev/null; then
@@ -69,22 +74,6 @@ if [ -f /var/run/reboot-required ]; then
audit_warn "a reboot is pending (/var/run/reboot-required) - schedule one"
fi
# --- Linux admin user with SSH key (for non-root SSH login) ---
LINUX_ADMIN_OK=0
for auth_file in /home/*/.ssh/authorized_keys; do
[ -f "$auth_file" ] || continue
# Must have at least one non-comment, non-empty key line.
if grep -qE '^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) ' "$auth_file" 2>/dev/null; then
LINUX_ADMIN_OK=1
break
fi
done
if [ "$LINUX_ADMIN_OK" -eq 1 ]; then
audit_pass "a non-root Linux user has an SSH authorized key"
else
audit_fail "no non-root Linux user has an authorized SSH key (run setup-linux-admin-user.sh)"
fi
# --- named admin user (not just root@pam) ---
if pveum user list --output-format json 2>/dev/null | grep -q '"userid":"[^"]*@pve"'; then
audit_pass "a named @pve admin user exists (root@pam is not the only account)"
@@ -92,23 +81,6 @@ else
audit_fail "no named @pve user found - root@pam is the only account"
fi
# --- passwordless sudo for pvesh/qm/pct ---
# The nixos flake's create-proxmox-resource.sh runs pvesh/qm/pct over
# non-interactive SSH, so the admin user needs NOPASSWD for these tools.
SUDO_OK=0
for f in /etc/sudoers.d/*-proxmox; do
[ -f "$f" ] || continue
if grep -qE 'NOPASSWD:.*pvesh' "$f" && grep -qE 'NOPASSWD:.*\bqm\b' "$f" && grep -qE 'NOPASSWD:.*\bpct\b' "$f"; then
SUDO_OK=1
break
fi
done
if [ "$SUDO_OK" -eq 1 ]; then
audit_pass "admin user has NOPASSWD sudo for pvesh/qm/pct"
else
audit_fail "no sudoers file grants NOPASSWD for pvesh/qm/pct (run setup-admin-sudo.sh <username>)"
fi
# --- time sync ---
if timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qx 'yes'; then
audit_pass "clock is NTP-synchronized"
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# Stage 1 base config + hardening, end to end, for a single fresh PVE host.
# Runs the individual scripts in order. Idempotent - safe to re-run.
#
# Does NOT create the named admin user (needs a username decision) - run
# create-admin-user.sh separately afterwards. Run audit.sh at the end to
# verify.
#
# Usage: MGMT_CIDR=192.168.2.0/24 ./bootstrap.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 [ -z "${MGMT_CIDR:-}" ]; then
echo "MGMT_CIDR is not set. Example: MGMT_CIDR=192.168.2.0/24 $0" >&2
exit 1
fi
echo "=== 1/5: remove enterprise repos, switch to no-subscription ==="
"${SCRIPT_DIR}/switch-to-no-subscription-repo.sh"
echo
echo "=== 2/5: SSH hardening (key-only root login + fail2ban) ==="
"${SCRIPT_DIR}/harden-ssh.sh"
echo
echo "=== 3/5: unattended security upgrades ==="
"${SCRIPT_DIR}/setup-unattended-upgrades.sh"
echo
echo "=== 4/5: PVE firewall (mgmt-only SSH/8006) ==="
MGMT_CIDR="$MGMT_CIDR" "${SCRIPT_DIR}/deploy-firewall.sh"
echo
echo "=== 5/5: disable subscription nag (cosmetic) ==="
"${SCRIPT_DIR}/disable-subscription-nag.sh"
echo
echo "=== Base hardening applied. Remaining manual/deliberate steps: ==="
echo " - ${SCRIPT_DIR}/create-admin-user.sh <username>"
echo " - Enable 2FA/TOTP for that user and root@pam via the web UI"
echo " - ${SCRIPT_DIR}/audit.sh (verify everything above)"
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env bash
# Scan the repo for secrets and sensitive config values.
# Runs via CI (GitHub/Gitea Actions) and locally as a pre-commit check.
#
# Usage: scripts/check-secrets.sh [--staged-only]
# --staged-only Only check files staged for commit (for pre-commit hook use)
#
# Requires gitleaks on PATH, or falls back to Docker if available.
# Install gitleaks: https://github.com/gitleaks/gitleaks#installing
set -euo pipefail
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
STAGED_ONLY=false
FAILURES=0
for arg in "$@"; do
[[ "$arg" == "--staged-only" ]] && STAGED_ONLY=true
done
cd "$REPO_ROOT"
# ── Resolve gitleaks binary ────────────────────────────────────────────────────
if command -v gitleaks &>/dev/null; then
GITLEAKS="gitleaks"
elif command -v docker &>/dev/null; then
GITLEAKS="docker run --rm -v ${REPO_ROOT}:/repo zricethezav/gitleaks:latest"
# Adjust paths for docker context
REPO_ROOT="/repo"
else
echo "ERROR: gitleaks not found. Install it or ensure Docker is available." >&2
echo " https://github.com/gitleaks/gitleaks#installing" >&2
exit 1
fi
echo "=== Secret scan ==="
if [[ "$STAGED_ONLY" == "true" ]]; then
# Pre-commit mode: scan only staged content
echo "Mode: staged files only"
if ! $GITLEAKS protect --staged --config="${REPO_ROOT}/.gitleaks.toml" --source="${REPO_ROOT}" 2>&1; then
FAILURES=$((FAILURES + 1))
fi
else
# CI mode: scan full git history
echo "Mode: full git history"
if ! $GITLEAKS detect --config="${REPO_ROOT}/.gitleaks.toml" --source="${REPO_ROOT}" 2>&1; then
FAILURES=$((FAILURES + 1))
fi
fi
# ── Pi-hole specific checks ────────────────────────────────────────────────────
echo ""
echo "=== Pi-hole config checks ==="
PIHOLE_TOML="${REPO_ROOT}/pihole/config/pihole.toml"
if [[ -f "$PIHOLE_TOML" ]]; then
# Check that known sensitive fields are empty
for field in pwhash totp_secret app_pwhash; do
value=$(grep -E "^\s+${field}\s*=" "$PIHOLE_TOML" | sed 's/.*=\s*"\(.*\)".*/\1/' | tr -d '[:space:]' || true)
if [[ -n "$value" && "$value" != '""' ]]; then
echo "FAIL: pihole.toml contains a non-empty '${field}' — run pihole/sanitize-config.sh before committing" >&2
FAILURES=$((FAILURES + 1))
else
echo " OK: ${field} is empty"
fi
done
else
echo " (pihole/config/pihole.toml not present, skipping Pi-hole checks)"
fi
# ── Summary ────────────────────────────────────────────────────────────────────
echo ""
if [[ $FAILURES -gt 0 ]]; then
echo "FAILED: ${FAILURES} issue(s) found. Fix before committing." >&2
exit 1
else
echo "All checks passed."
fi
File renamed without changes.
File renamed without changes.
File renamed without changes.
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
# Install git hooks that run the secret scan before every commit.
# Run once after cloning: bash scripts/install-hooks.sh
set -euo pipefail
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
HOOK="${REPO_ROOT}/.git/hooks/pre-commit"
cat > "$HOOK" << 'HOOK'
#!/usr/bin/env bash
exec "$(git rev-parse --show-toplevel)/scripts/check-secrets.sh" --staged-only
HOOK
chmod +x "$HOOK"
echo "Installed pre-commit hook → ${HOOK}"
echo "The secret scan will run automatically before every commit."
File renamed without changes.
File renamed without changes.