diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 9473228..b70681b 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -108,3 +108,11 @@ never banned); 5/10m/1h from RECOVERY doc 01. (2) `20auto-upgrades` file instead install continues. Declined by user: auditd rules, ufw whitelist (site-specific ports, lockout risk → would be an offer, not systematic). Not included by design: PasswordAuthentication no / AllowUsers / X11Forwarding no (lockout or workflow risk). Status: done in repo (feature/security-baseline), live apply = user. + +## BDR-014 — install.sh mirrors machine apt set: GNOME + LAMP unconditional, NVIDIA via ubuntu-drivers +2026-09-28. Source: `apt-mark showmanual` + /var/log/apt/history.log diffed vs script. Added gitleaks, web stack +(mariadb-server imagemagick php-* unversioned → follows distro PHP), ubuntu-desktop-minimal before RDP setup +(gnome-remote-desktop needs GDM, bare server had none), `install_nvidia_driver()` = `lspci -d 10de:` gate + +`ubuntu-drivers install` (distro-recommended, 595-open today). Alternatives rejected: pin nvidia-driver-595-open +(ages, hardware-bound), LAMP behind confirm() offer (user: base list), GNOME left implicit (RDP fails silently). +Status: merged to develop. Live rerun of install.sh = user. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 78f49c8..7bab278 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -68,3 +68,13 @@ Cleanup: user asked all-in-develop + delete branches. Hooks refresh committed (a branches verified merged; `git push --delete` DENIED by permission layer → user runs it. gitea-deploy/ (untracked Gitea server deploy project, 31 files, no secrets) moved to ~/Documents/gitea-deploy, own repo via gitflow init, pushed main+develop to git.bchanot.fr (push-to-create worked). deploy.conf gitignored. + +## 2026-09-28 +- feature/apt-packages (0bc9e3f, unmerged): install.sh mirrors machine apt set. Diff `apt-mark showmanual` + +apt history vs script → added gitleaks, web stack (mariadb-server imagemagick php-* unversioned), +ubuntu-desktop-minimal before RDP, install_nvidia_driver() (lspci 10de gate, `ubuntu-drivers install`, no pin). +User approved 3 choices (GNOME in, ubuntu-drivers, LAMP unconditional). shellcheck + bash -n + stub run OK. +- Blocked mid-commit: lib pre-commit ran `gitleaks git --staged`, Ubuntu apt gitleaks = 8.16 (no `git` subcmd, +exit 1 read as leak). Fixed in claude-config bugfix/gitleaks-protect-fallback (347073a, unmerged): probe +`gitleaks git --help`, fallback `protect --staged`; T16c symlink-farm PATH. make test 0. Hooks refreshed here (9e49b9d). +- Note: `gh` in install.sh list but not installed on this box (script not rerun since added). diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 6f5d05e..70d5ca5 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -93,3 +93,11 @@ templating, drifts if port changes) or `banaction = %(banaction_allports)s` (off irrelevant) — chose allports. Offline checks without fail2ban installed: python `configparser` with BasicInterpolation resolves `%(x)s` refs and proves the file parses; apt.conf → `apt-config --config-file= dump APT::Periodic`. sshd drop-ins can't be `sshd -t`-tested without root (host keys). + +## LRN-013 — distro package lags upstream: probe subcommand before calling it +2026-09-28. Ubuntu apt gitleaks = 8.16; lib pre-commit hook written for >= 8.19 (`gitleaks git --staged`). +"unknown command" exit 1 read as a leak → every commit blocked, silently (stderr swallowed). Script calling a +subcommand born in version N must probe `tool sub --help` and fall back (`protect --staged`). Test faking +"binary absent" via shorter PATH (`/usr/bin:/bin`) breaks once the binary lives in /usr/bin: symlink farm of +/usr/bin minus the binary instead. Apply: any tool install.sh pulls from apt while ~/.claude scripts assume +the upstream release. Fix: claude-config bugfix/gitleaks-protect-fallback. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 66e41ee..a61c1f3 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -60,3 +60,12 @@ Branch: feature/security-baseline (off develop). Scope approved: fail2ban, unatt - [x] README.md (table, step 13, packages) + CLAUDE.md layout - [x] shellcheck + bash -n; stub harness harden_sshd (accept / reject paths); configparser check of jail file - [x] commit; registries (BDR-013, LRN-012); runbook. No finish without explicit signal. + +## Feature — install.sh mirrors this machine's apt packages (2026-09-28) +Branch: feature/apt-packages (off develop). Source: apt-mark showmanual + /var/log/apt/history.log diffed against install.sh. +- [x] gitleaks in the base list (backs the pre-commit hook) +- [x] web stack group: mariadb-server imagemagick + unversioned php-* modules (approved: base list, not an offer) +- [x] ubuntu-desktop-minimal before setup_remote_desktop (approved) +- [x] install_nvidia_driver(): lspci vendor 10de gate + ubuntu-drivers install (approved: no version pin) +- [x] README steps 11 + packages; shellcheck + bash -n; stub run of the NVIDIA helper +- [x] commit on the feature branch. No finish without explicit signal. diff --git a/.githooks/pre-commit b/.githooks/pre-commit index ef3abef..a42373a 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -9,10 +9,15 @@ git rev-parse --verify -q HEAD >/dev/null 2>&1 || exit 0 # root commit — all # Secret backstop (job7) — any branch, not just protected ones. Non-blocking # if gitleaks isn't installed; auto-discovers ./.gitleaks.toml (repo root). +# gitleaks >= 8.19 scans the index with `git --staged`; older builds (Ubuntu's +# 8.16 package) only know `protect --staged`, and `git` exits 1 there as an +# unknown command — which would block every commit. Probe the subcommand first. if command -v gitleaks >/dev/null 2>&1; then - if ! gitleaks git --staged --no-banner >/dev/null 2>&1; then + gl_sub=git + gitleaks git --help >/dev/null 2>&1 || gl_sub=protect + if ! gitleaks "$gl_sub" --staged --no-banner >/dev/null 2>&1; then echo "gitflow pre-commit: BLOCKED — gitleaks found a secret in staged changes." >&2 - echo " Details: gitleaks git --staged --no-banner" >&2 + echo " Details: gitleaks $gl_sub --staged --no-banner" >&2 echo " Genuine false-positive? add an allowlist rule to .gitleaks.toml — never bypass with --no-verify." >&2 exit 1 fi diff --git a/.githooks/reference-transaction b/.githooks/reference-transaction new file mode 100755 index 0000000..1e2cab1 --- /dev/null +++ b/.githooks/reference-transaction @@ -0,0 +1,15 @@ +#!/bin/sh +# gitflow reference-transaction — generated by gitflow_init. Do not hand-edit. +# Refuses deleting (or renaming) main / develop, whatever the +# command. Mirrors gitflow_protected_base (lib/gitflow.sh). +[ "$1" = prepared ] || exit 0 +while read -r _old new ref; do + case "$ref" in refs/heads/main|refs/heads/develop) ;; *) continue ;; esac + case "$new" in *[!0]*) continue ;; esac # new value not all-zeros → an update, not a deletion + # Per-repo opt-out (a foreign clone): git config gitflow.protect false + [ "$(git config --bool --default true gitflow.protect)" = false ] && exit 0 + echo "gitflow reference-transaction: BLOCKED — deleting '$ref', a protected base." >&2 + echo " main and develop are never deleted or renamed. A merged working branch: gitflow.sh delete " >&2 + exit 1 +done +exit 0 diff --git a/README.md b/README.md index 3f35e5e..1561106 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ What it does: 8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. The dtach session-resume menu ships in the deployed `bashrc-linux`, so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read `~/.profile`. The installer also strips any older dtach block left in `~/.profile` so a plain SSH login doesn't prompt twice. 9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage. 10. On Linux, installs **code-server** (VS Code in the browser) via its vendor script — skipped if already present — and enables the `code-server@$USER` systemd service. -11. On Linux, sets up **RDP remote login** via `gnome-remote-desktop` (Wayland-native): installs the daemon + `openssl`, generates a self-signed TLS cert once, and prompts interactively for shared "gate" credentials (skipped when no terminal is attached, or already set). Disables `xrdp` if present; opens UFW port `3389` only when UFW is already active. +11. On Linux, installs **`ubuntu-desktop-minimal`** (GDM + GNOME Shell, ~1.5 GB): the RDP remote login below hands out a GNOME session, which a bare server install does not have. Then sets up **RDP remote login** via `gnome-remote-desktop` (Wayland-native): installs the daemon + `openssl`, generates a self-signed TLS cert once, and prompts interactively for shared "gate" credentials (skipped when no terminal is attached, or already set). Disables `xrdp` if present; opens UFW port `3389` only when UFW is already active. Finally, when `lspci` sees an NVIDIA GPU, runs `ubuntu-drivers install` to put on the driver the distro recommends for the card (no version pinned; loads at the next reboot). Skipped on machines without an NVIDIA GPU. 12. On Linux, installs the **`cloudpex`** NAS mount helper to `/usr/local/bin` via `cloudpex/install.sh`, which prompts for the NAS host, share name, SMB user, mount point and SMB version and writes them to `/etc/cloudpex.conf` (root, `0600`; an existing config is shown and kept unless you say `n`; skipped when no terminal is attached). Nothing is mounted, no password stored, see [`cloudpex/README.md`](cloudpex/README.md). 13. On Linux, installs the **security baseline**, always, no prompt: **fail2ban** (+ `nftables`) with `etc/fail2ban/jail.d/local.conf` (sshd jail reading the journal, bans the offending IP on every port so the SSH port does not matter, 5 failures in 10 min → 1 h ban, loopback and private LAN ranges never banned); **unattended-upgrades** enabled through `etc/apt/apt.conf.d/20auto-upgrades`; and the **sshd drop-in** `etc/ssh/sshd_config.d/20-hardening.conf` (`PermitRootLogin no`, `MaxAuthTries 3`, `LoginGraceTime 20`), checked with `sshd -t` and removed again if sshd rejects it, then `reload ssh`. Authentication methods, port and user lists are left as they are. 14. On Linux, at the very end, **offers** (`[y/N]`, skipped when no terminal is attached) to move **`/tmp` to disk**: Ubuntu mounts `/tmp` as a RAM-backed tmpfs capped at 50% of RAM, which agent runs fill, halving the RAM and breaking every shell with "No space left on device". Accepting masks `tmp.mount` and installs `etc/tmpfiles.d/tmp.conf` (wipe at boot, 10-day purge). Effective at the next reboot. @@ -75,12 +75,14 @@ What it does: ### Packages installed (apt) -- **Build / VCS / C dev**: `vim git git-lfs git-filter-repo gcc make pkg-config dkms valgrind shellcheck` +- **Build / VCS / C dev**: `vim git git-lfs git-filter-repo gitleaks gcc make pkg-config dkms valgrind shellcheck gh` - **Net / security / transport**: `curl gnupg ca-certificates apt-transport-https net-tools openssh-server cifs-utils lftp ftp` - **Shell tooling**: `unzip tree tmux fzf dtach` - **Runtimes**: `nodejs python3-pip pipx php-cli` +- **Web stack (local WordPress/LAMP)**: `mariadb-server imagemagick php-mysql php-gd php-imagick php-mbstring php-xml php-intl php-curl` (unversioned `php-*` metapackages, so they follow the distro's PHP) - **Media / doc CLI**: `ffmpeg weasyprint poppler-utils qpdf webp libavif-bin` - **Docker**: `docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin` (via Docker's repo) +- **Desktop / GPU**: `ubuntu-desktop-minimal` (always, Linux) + the distro-recommended NVIDIA driver via `ubuntu-drivers install` (only when an NVIDIA GPU is detected) - **Remote access**: `gnome-remote-desktop openssl` (apt) + `code-server` (via its vendor install script, not apt) — RDP remote login + browser VS Code - **pipx**: `PyMuPDF` (`pymupdf`), `Markdown` (`markdown_py`) - **Security baseline (Linux, always)**: `fail2ban nftables unattended-upgrades` diff --git a/install.sh b/install.sh index 663296d..79aa6c2 100755 --- a/install.sh +++ b/install.sh @@ -27,6 +27,25 @@ install_docker() { sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin } +# NVIDIA driver: only when an NVIDIA GPU is on the PCI bus (vendor id 10de), so the +# script stays hardware-agnostic elsewhere. ubuntu-drivers picks the driver the distro +# recommends for the card (595-open on the RTX 3060 Ti this was written on) instead of +# pinning a version that ages. Idempotent: a no-op when the recommended driver is in. +# The driver loads at the next reboot. Ubuntu-only (ubuntu-drivers-common). +install_nvidia_driver() { + if ! command -v ubuntu-drivers >/dev/null 2>&1; then + echo "ubuntu-drivers not found — skipping NVIDIA driver" >&2 + return 0 + fi + if [ -z "$(lspci -d 10de: 2>/dev/null)" ]; then + echo "No NVIDIA GPU detected — skipping NVIDIA driver" + return 0 + fi + echo "NVIDIA GPU detected — installing the recommended driver" + sudo ubuntu-drivers install + echo "NVIDIA driver loads at the next reboot." +} + # RDP "gate" credentials: a shared username/password that unlocks the GDM # login screen (each user then logs into GDM with his own account). Required — # without it the RDP server rejects every connection (mstsc error 0x904). It is @@ -259,14 +278,18 @@ if command -v apt-get >/dev/null 2>&1; then sudo apt-get update sudo apt-get upgrade -y - # Build + version control + C dev tooling. + # Build + version control + C dev tooling (gitleaks backs the pre-commit hook). + # Web stack: MariaDB + PHP modules for local WordPress/LAMP work; the php-* metapackages + # follow the distro's PHP version instead of pinning php8.x-*. sudo apt-get install -y \ - vim git git-lfs git-filter-repo gcc make pkg-config dkms valgrind shellcheck \ + vim git git-lfs git-filter-repo gitleaks gcc make pkg-config dkms valgrind shellcheck \ curl gnupg ca-certificates apt-transport-https \ unzip tree tmux fzf dtach net-tools \ openssh-server cifs-utils lftp ftp \ nodejs python3-pip pipx php-cli \ - ffmpeg weasyprint poppler-utils qpdf webp libavif-bin gh + ffmpeg weasyprint poppler-utils qpdf webp libavif-bin gh \ + mariadb-server imagemagick \ + php-mysql php-gd php-imagick php-mbstring php-xml php-intl php-curl # Docker (separate repo). install_docker @@ -277,9 +300,16 @@ if command -v apt-get >/dev/null 2>&1; then fi sudo systemctl enable --now "code-server@$USER" + # GNOME desktop (GDM + Shell): the RDP remote login below needs a GNOME session + # to hand out; a bare server install has none. Ubuntu-only metapackage. + sudo apt-get install -y ubuntu-desktop-minimal + # Remote desktop (gnome-remote-desktop — see the function header for why not xrdp). setup_remote_desktop + # NVIDIA driver, only when an NVIDIA GPU is present. + install_nvidia_driver + # Low-disk login warning (system-wide profile.d snippet). install_disk_warning