From 9c5669027019a08cbec4df3a880c05e19b6592c1 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Wed, 10 Jun 2026 20:19:05 -0700 Subject: [PATCH] sync: auto-sync from GURU-5070 at 2026-06-10 20:18:48 Author: Mike Swanson Machine: GURU-5070 Timestamp: 2026-06-10 20:18:48 --- .claude/commands/vault.md | 36 ++++ .../feedback_ascii_only_api_payloads.md | 26 +++ .../reference_backblaze_storage_rate.md | 12 ++ .../reference_sqlx_migrations_immutable.md | 33 ++++ .claude/skills/vault/SKILL.md | 136 +++++++++++++ .claude/skills/vault/scripts/vault-helper.sh | 180 ++++++++++++++++++ clients/vons-carstar/cloud/azure.md | 28 +++ clients/vons-carstar/cloud/m365.md | 51 +++++ clients/vons-carstar/issues/log.md | 19 ++ clients/vons-carstar/network/dhcp.md | 31 +++ clients/vons-carstar/network/dns.md | 33 ++++ clients/vons-carstar/network/firewall.md | 47 +++++ clients/vons-carstar/network/topology.md | 43 +++++ clients/vons-carstar/network/vlans.md | 21 ++ clients/vons-carstar/overview.md | 34 ++++ clients/vons-carstar/rmm/rmm.md | 34 ++++ clients/vons-carstar/security/antivirus.md | 26 +++ clients/vons-carstar/security/backup.md | 34 ++++ .../vons-carstar/servers/server_template.md | 49 +++++ 19 files changed, 873 insertions(+) create mode 100644 .claude/commands/vault.md create mode 100644 .claude/memory/feedback_ascii_only_api_payloads.md create mode 100644 .claude/memory/reference_backblaze_storage_rate.md create mode 100644 .claude/memory/reference_sqlx_migrations_immutable.md create mode 100644 .claude/skills/vault/SKILL.md create mode 100644 .claude/skills/vault/scripts/vault-helper.sh create mode 100644 clients/vons-carstar/cloud/azure.md create mode 100644 clients/vons-carstar/cloud/m365.md create mode 100644 clients/vons-carstar/issues/log.md create mode 100644 clients/vons-carstar/network/dhcp.md create mode 100644 clients/vons-carstar/network/dns.md create mode 100644 clients/vons-carstar/network/firewall.md create mode 100644 clients/vons-carstar/network/topology.md create mode 100644 clients/vons-carstar/network/vlans.md create mode 100644 clients/vons-carstar/overview.md create mode 100644 clients/vons-carstar/rmm/rmm.md create mode 100644 clients/vons-carstar/security/antivirus.md create mode 100644 clients/vons-carstar/security/backup.md create mode 100644 clients/vons-carstar/servers/server_template.md diff --git a/.claude/commands/vault.md b/.claude/commands/vault.md new file mode 100644 index 0000000..fbb5fdb --- /dev/null +++ b/.claude/commands/vault.md @@ -0,0 +1,36 @@ +# /vault — Consistent SOPS vault operations + +The one canonical way to read, store, update, and verify secrets in the ClaudeTools SOPS+age +vault. Use instead of raw `sops` or guessed paths. Full reference: `.claude/skills/vault/SKILL.md`. + +## Quick reference + +```bash +# READ +bash .claude/scripts/vault.sh get +bash .claude/scripts/vault.sh get-field credentials.api_key +bash .claude/scripts/vault.sh search +bash .claude/scripts/vault.sh list [subdir] + +# STORE / UPDATE (non-interactive — these work in this harness; `vault edit` does not) +bash .claude/skills/vault/scripts/vault-helper.sh new --kind api-key \ + --name "..." [--url ..] [--tag ..] --set api_key=SECRET [--set username=foo] +bash .claude/skills/vault/scripts/vault-helper.sh set --set password=NEW + +# VERIFY (after any write, before any commit) +bash .claude/skills/vault/scripts/vault-helper.sh verify +bash .claude/skills/vault/scripts/vault-helper.sh check [subdir] + +# PUBLISH +bash .claude/scripts/sync.sh # Phase 6 commits + pushes the vault repo +``` + +## Rules (non-negotiable) + +1. Never paste a secret into chat / ticket / commit / channel — share the vault path instead. +2. Secrets ALWAYS go under `credentials:` (only those keys get encrypted; anything else = plaintext). +3. Use the scripts above — never hand-roll `sops` + a guessed path, never use `VAULT_ROOT_ENV` for vault access. +4. Finish: write → `verify` → publish (sync). Don't hand off the push. + +Paths are vault-root-relative (`clients//...`, `msp-tools/...`, `infrastructure/...`, +`services/...`), with or without `.sops.yaml`. diff --git a/.claude/memory/feedback_ascii_only_api_payloads.md b/.claude/memory/feedback_ascii_only_api_payloads.md new file mode 100644 index 0000000..71b77a7 --- /dev/null +++ b/.claude/memory/feedback_ascii_only_api_payloads.md @@ -0,0 +1,26 @@ +--- +name: feedback_ascii_only_api_payloads +description: On Windows/Git-bash, non-ASCII chars (em-dash, arrow, smart quotes) in JSON payload TEXT passed to curl get mangled and rejected — Discord bot-alert returns 400, the coord API returns "error parsing the body". Use ASCII-only in API payload text, or a single-quoted heredoc. +metadata: + type: feedback +--- + +When building JSON API payloads on Windows/Git-bash and sending via `curl`, **non-ASCII characters +in the text fields get mangled in transit and rejected by the server**, even though `jq -n` +produces valid UTF-8 JSON. Hit twice on 2026-06-01: +- `post-bot-alert.sh` → Discord **400** `{"message":"The request body contains invalid JSON","code":50109}` on a message containing `—` (em-dash) and `→` (arrow). +- Coord todos API (`POST /api/coord/todos`) → **`{"detail":"There was an error parsing the body"}`** on todo text containing em-dashes (both the inline `$(jq -n ...)` and the `P=$(jq -n ...); curl --data-binary "$P"` patterns failed). + +**Why:** the round-trip through a bash variable → `curl --data-binary` re-encodes/mangles the +multibyte UTF-8 (Git-bash codepage quirk), so the bytes the server receives are no longer valid JSON. + +**Fix:** keep API payload text **ASCII-only** — use `-` not `—`, `->` not `→`, straight quotes not +smart quotes. The most robust transport is a **single-quoted heredoc** piped to curl: +```bash +curl -s -X POST "$API" -H "Content-Type: application/json" --data-binary @- <<'JSON' +{"text":"ASCII only - no em-dashes or arrows","project_key":"..."} +JSON +``` +This bit the Syncro bot-alert (resolved by ASCII retry) and the coord-todo filings the same day. +NOTE-TO-SELF tie-in: the project's NO-EMOJIS rule already pushes ASCII markers; extend that habit to +all API payload text, not just console output. diff --git a/.claude/memory/reference_backblaze_storage_rate.md b/.claude/memory/reference_backblaze_storage_rate.md new file mode 100644 index 0000000..5b1dcfc --- /dev/null +++ b/.claude/memory/reference_backblaze_storage_rate.md @@ -0,0 +1,12 @@ +--- +name: reference_backblaze_storage_rate +description: ACG's Backblaze B2 storage cost rate ($0.00695/GB) for the GuruRMM mspbackups storage-cost calculation +metadata: + type: reference +--- + +ACG's Backblaze B2 storage rate is **$0.00695 per GB**. Use this as the cost input when calculating client storage cost in the GuruRMM **mspbackups** (MSP360) ability. + +- Cost = stored_GB x 0.00695 (USD). +- This is ACG's cost basis; client-facing markup/billing is a separate decision, not this figure. +- The B2 storage-management credential is the vault entry `projects/claudetools/backblaze-b2.sops.yaml` (key name "ClaudeTools", manages buckets/keys for the mspbackups feature). diff --git a/.claude/memory/reference_sqlx_migrations_immutable.md b/.claude/memory/reference_sqlx_migrations_immutable.md new file mode 100644 index 0000000..c381a9a --- /dev/null +++ b/.claude/memory/reference_sqlx_migrations_immutable.md @@ -0,0 +1,33 @@ +--- +name: reference_sqlx_migrations_immutable +description: NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration. +metadata: + type: reference +--- + +GuruRMM and GuruConnect both apply DB migrations at server startup via `sqlx::migrate!()` +(embedded at COMPILE time from `server/migrations/`). sqlx stores a **checksum** of each migration +in the `_sqlx_migrations` table when it first applies it, and on every startup re-validates the +embedded migration files' checksums against that table. + +**Editing an already-applied migration file — even just a COMMENT — changes its checksum** and the +server fails to boot: +``` +ERROR Failed to run migrations: migration 8 was previously applied but has been modified +``` +systemd then crash-loops it and eventually trips the start-limit ("Start request repeated too quickly"). + +**Incident 2026-06-01 (GuruConnect):** a one-line `ON CONFLICT` fix in `server/src/db/machines.rs` +was bundled with a *comment-only* edit to `server/migrations/008_machine_uid.sql`. The code fix was +correct, but the migration comment edit took the relay down for ~6 min on deploy. Both the Coding +Agent and the Code Review Agent explicitly judged the comment edit "zero runtime effect" — WRONG. + +**Rules:** +- Applied migrations are **immutable**. Never touch them. To change schema, write a NEW migration. +- If documentation about a migration needs fixing, put it in code comments / docs, NOT the migration file. +- **Code review must reject ANY diff that touches a file under `server/migrations/` that has already + been applied in prod** (or require a brand-new migration instead). +- **Recovery:** restore the migration's exact original bytes (`git checkout -- path/to/NNN.sql`), + rebuild (sqlx embeds at compile time, so a rebuild is required), restart. If systemd shows + "Start request repeated too quickly", clear the limiter first: `sudo systemctl reset-failed ` + then `sudo systemctl start `. diff --git a/.claude/skills/vault/SKILL.md b/.claude/skills/vault/SKILL.md new file mode 100644 index 0000000..98f53fa --- /dev/null +++ b/.claude/skills/vault/SKILL.md @@ -0,0 +1,136 @@ +--- +name: vault +description: "The ONE canonical way to use the ClaudeTools SOPS+age secret vault — read, store, update, and verify credentials. Use this whenever a task involves a password, API key, token, secret, connection string, SSH key, or any credential: retrieving one to use it, storing a newly created/discovered one, or checking what's vaulted. Stops the per-session improvising (raw sops, guessed paths, VAULT_ROOT_ENV hacks, plaintext-field mistakes). Triggers: vault, store/save a secret, add to vault, get the password/api key for X, where is the credential for X, sops, encrypt this secret, decrypt, rotate a credential, 1password fallback, vault a new key." +--- + +# Vault — one consistent way to handle secrets + +Vaulting should be identical every time. It is. This skill is the single source of truth so no +session has to re-derive it. Two tools, one rule: + +- **Reads / search / list** → the canonical `vault.sh` (auto-resolves the vault from identity.json). +- **Create / update / verify (non-interactive)** → `vault-helper.sh` in this skill. (The base + `vault.sh add`/`edit` are interactive `$EDITOR` flows Claude can't drive — do NOT use them; do + NOT fall back to raw `sops` with hand-built paths.) + +## THE RULE (read this first) + +1. **Never paste a secret into chat, a ticket, a commit message, or a coord/Discord channel.** + Point people at the vault path instead (teammates with vault access decrypt it themselves). +2. **Never write a secret to a field outside `credentials:`.** The `.sops.yaml` only encrypts keys + named `credentials | password | secret | api_key | token | pre_shared_key | notes | content`. + A secret placed anywhere else commits in **plaintext**. Always put secrets under `credentials:`. +3. **Never hand-roll the vault path or `sops` command.** Use the two scripts below — they resolve + the vault root the same way on every machine. +4. **Finish the job:** create/update → verify it's encrypted → publish (sync). Don't stop at + "it's on disk, you push it." + +## Read a secret (canonical) + +```bash +# whole entry (decrypted) +bash .claude/scripts/vault.sh get +# one field, dot-notation (e.g. credentials.api_key, credentials.admin.password) +bash .claude/scripts/vault.sh get-field credentials.api_key +# find where something lives +bash .claude/scripts/vault.sh search +bash .claude/scripts/vault.sh list [subdir] +``` + +`` is relative to the vault root, with or without the `.sops.yaml` suffix +(e.g. `clients/kittle/gururmm-site-main` or `msp-tools/computerguru-user-manager.sops.yaml`). + +The repo wrapper `.claude/scripts/vault.sh` reads `vault_path` from `.claude/identity.json` and +delegates to the real `vault.sh` in the vault repo. That is the ONLY entry point you need for +reads. (If `vault_path` is missing on a machine, fix identity.json — don't paper over it with +`VAULT_ROOT_ENV`, which is a separate remediation-tool-script quirk, not how you read the vault.) + +## Store a NEW secret (non-interactive, one shot) + +```bash +bash .claude/skills/vault/scripts/vault-helper.sh new \ + --kind \ + --name "Human-readable name" [--url https://...] [--tag client] [--tag service] \ + --set api_key=THE_SECRET [--set username=foo] [--set password=bar] +``` + +This writes the plaintext template (metadata at top level, every `--set` under `credentials:`), +encrypts it in place with `sops`, verifies the round-trip, and tells you to publish. It refuses if +the file already exists. + +## Update / add a field on an existing entry + +```bash +bash .claude/skills/vault/scripts/vault-helper.sh set --set password=NEW_VALUE +``` + +Decrypts, merges the field(s) into `credentials:`, re-encrypts, verifies. Use this instead of +`vault edit` (which needs an interactive editor). + +## Verify (always, after any write — and before any commit) + +```bash +bash .claude/skills/vault/scripts/vault-helper.sh verify # one entry: encrypted + decrypts +bash .claude/skills/vault/scripts/vault-helper.sh check [subdir] # scan for ANY plaintext *.sops.yaml +``` + +`check` is the safety net against the plaintext footgun — run `check` over a dir (or the whole +vault) before committing if you hand-edited anything. + +## Publish (the last mile — do it yourself) + +The main sync handles the vault repo too: + +```bash +bash .claude/scripts/sync.sh # Phase 6 commits + pushes the vault repo +``` + +(Equivalent: `cd && git add -A && git commit -m "..." && git push`.) Don't park +"you push it" as a task — a clean encrypted entry is routine. Only hand off if `git push` itself +fails (auth/conflict). The Windows LF→CRLF warning on the yaml is benign — SOPS integrity is over +the `ENC[...]` values, not line endings. + +## Layout & file format + +Vault root subdirs: `clients//`, `msp-tools/`, `infrastructure/`, `services/`, `projects/`, +`business/`, `ssh-keys/`, `tailscale/`. Put a client credential under `clients//`, an MSP app +under `msp-tools/`, shared infra under `infrastructure/` or `services/`. + +A decrypted entry looks like: + +```yaml +kind: api-key # api-key | server | m365-tenant | vpn | note | generic +name: Human-readable name +url: https://... # optional, plaintext metadata +status: active +tags: [client, service] # plaintext, searchable +credentials: # <-- EVERYTHING secret goes here (this whole block is encrypted) + api_key: "..." + username: "..." + password: "..." +notes: "" # encrypted too +``` + +Plaintext metadata (kind/name/url/tags/status and arbitrary non-secret structure like `client:` / +`site:`) stays readable so `search` works. Only `credentials`/`notes` (and the other regex keys) +are encrypted. + +## 1Password fallback + +The SOPS vault is primary. 1Password is the fallback when a secret isn't in SOPS or for +human-shared items — use the `1password` skill / `op` CLI for that. If you store something new and +it belongs in the team flow, prefer the SOPS vault so it syncs with the repo. + +## Gotchas (already handled — don't re-discover them) + +- **Interactive `vault edit` / `vault add`** don't work in this harness ($EDITOR). Use + `vault-helper.sh set` / `new` instead. +- **yq blocked on Windows (WDAC/Device Guard)** — `vault.sh` auto-falls back to a bundled Python + YAML parser. Nothing to do. +- **`VAULT_ROOT_ENV`** is only a workaround for the *remediation-tool* scripts mis-resolving their + root; it is NOT the vault access pattern. For vault work use the two scripts here. +- **Encrypted-field regex** is the one real footgun — secrets must be under `credentials:` (or a + top-level `password`/`api_key`/`token`/`secret`/`pre_shared_key`/`notes`/`content`). `verify` + catches a miss. +- The vault repo has its own pre-commit `harness-guard`/hook that warns on plaintext; `check` is + your proactive version of the same. diff --git a/.claude/skills/vault/scripts/vault-helper.sh b/.claude/skills/vault/scripts/vault-helper.sh new file mode 100644 index 0000000..d890ba5 --- /dev/null +++ b/.claude/skills/vault/scripts/vault-helper.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# vault-helper.sh — non-interactive safety rails on top of the canonical vault.sh. +# +# Why this exists: the base vault.sh `add`/`edit` flow is interactive ($EDITOR), +# which Claude Code cannot drive — so sessions improvise with raw `sops` and get +# it wrong (plaintext fields, wrong paths, forgotten encrypt). This wrapper does +# create / set / verify NON-interactively and refuses to leave a secret in +# plaintext. Reads delegate to the canonical vault.sh. +# +# Commands: +# new --kind [--name "..."] [--url "..."] [--tag T]... --set k=v [--set k=v]... +# Create a new ENCRYPTED entry in one shot. +# set --set k=v [--set k=v]... +# Add/update credential field(s) on an existing entry. +# verify Assert one entry is encrypted + round-trips. +# check [dir] Scan a dir (default whole vault) for any *.sops.yaml +# that is NOT encrypted (catches plaintext leaks). +# get [field] Delegate to vault.sh get / get-field (read). +# find Delegate to vault.sh search (read). +# list [dir] Delegate to vault.sh list (read). +# root Print the resolved vault root + how it was found. +# +# Secrets ALWAYS go under `credentials:` (the .sops.yaml encrypted_regex encrypts +# the keys: credentials|password|secret|api_key|token|pre_shared_key|notes|content). +# Anything you put OUTSIDE those keys is committed in PLAINTEXT — never do that. +set -euo pipefail + +# ── Resolve vault root (the #1 thing sessions get wrong) ────────────────────── +# Order: $VAULT_PATH override → the repo we're standing in (correct identity) → +# $HOME identity vault_path → $HOME identity claudetools_root → that repo's identity. +resolve_vault() { + local how p root idf + if [[ -n "${VAULT_PATH:-}" && -d "${VAULT_PATH}" ]]; then echo "$VAULT_PATH|env:VAULT_PATH"; return 0; fi + if [[ -n "${VAULT_ROOT_ENV:-}" && -d "${VAULT_ROOT_ENV}" ]]; then echo "$VAULT_ROOT_ENV|env:VAULT_ROOT_ENV"; return 0; fi + # repo we're standing in (most reliable: uses the in-repo identity.json that has vault_path) + if root=$(git rev-parse --show-toplevel 2>/dev/null); then + idf="$root/.claude/identity.json" + if [[ -f "$idf" ]]; then + p=$(_id_field "$idf" vault_path); [[ -n "$p" && -d "$p" ]] && { echo "$p|repo-identity:$idf"; return 0; } + fi + fi + # home identity vault_path + idf="$HOME/.claude/identity.json" + if [[ -f "$idf" ]]; then + p=$(_id_field "$idf" vault_path); [[ -n "$p" && -d "$p" ]] && { echo "$p|home-identity"; return 0; } + # home identity -> claudetools_root -> repo identity vault_path + local ctr; ctr=$(_id_field "$idf" claudetools_root) + if [[ -n "$ctr" && -f "$ctr/.claude/identity.json" ]]; then + p=$(_id_field "$ctr/.claude/identity.json" vault_path); [[ -n "$p" && -d "$p" ]] && { echo "$p|home->repo-identity"; return 0; } + fi + fi + return 1 +} + +_id_field() { # + local f="$1" k="$2" v="" + if command -v jq >/dev/null 2>&1; then v=$(jq -r --arg k "$k" '.[$k] // empty' "$f" 2>/dev/null); fi + if [[ -z "$v" ]]; then + local fp="$f"; command -v cygpath >/dev/null 2>&1 && fp=$(cygpath -m "$f") + local py; for py in py python3 python; do command -v "$py" >/dev/null 2>&1 && { v=$("$py" -c "import json;print(json.load(open(r'$fp')).get('$k',''))" 2>/dev/null) && break; }; done + fi + echo "$v" +} + +VR_RAW=$(resolve_vault) || { echo "[ERROR] Could not resolve the vault root." >&2 + echo " Set \$VAULT_PATH, or add vault_path to .claude/identity.json, or run from inside the ClaudeTools repo." >&2; exit 3; } +VAULT_DIR="${VR_RAW%%|*}" +VAULT_HOW="${VR_RAW##*|}" + +VAULT_SH="$VAULT_DIR/scripts/vault.sh" # canonical read tool (delegate to it) + +_py() { local p; for p in py python3 python; do command -v "$p" >/dev/null 2>&1 && { echo "$p"; return 0; }; done; return 1; } +PY=$(_py) || { echo "[ERROR] python (py/python3/python) required" >&2; exit 3; } + +abspath() { # path arg -> absolute vault file (.sops.yaml appended if missing) + local p="$1"; [[ "$p" == *.sops.yaml ]] || p="$p.sops.yaml" + echo "$VAULT_DIR/$p" +} + +# assert a file on disk is encrypted (has ENC[ blobs) and round-trips +_is_encrypted() { grep -q 'ENC\[AES256_GCM' "$1" 2>/dev/null; } + +cmd_verify() { + local f; f=$(abspath "${1:?usage: verify }") + [[ -f "$f" ]] || { echo "[ERROR] not found: $f" >&2; exit 1; } + if ! _is_encrypted "$f"; then echo "[FAIL] $1 — NO encrypted values found (plaintext?)"; exit 1; fi + if ! ( cd "$VAULT_DIR" && sops -d "$f" >/dev/null 2>&1 ); then echo "[FAIL] $1 — encrypted but does not decrypt (key mismatch?)"; exit 1; fi + echo "[OK] $1 — encrypted and decrypts cleanly" +} + +cmd_check() { + local dir="${1:-}"; local scan="$VAULT_DIR"; [[ -n "$dir" ]] && scan="$VAULT_DIR/$dir" + local bad=0 total=0 f + while IFS= read -r -d '' f; do + total=$((total+1)) + if ! _is_encrypted "$f"; then echo "[PLAINTEXT] ${f#$VAULT_DIR/}"; bad=$((bad+1)); fi + done < <(find "$scan" -name '*.sops.yaml' -type f -print0 2>/dev/null) + if [[ $bad -eq 0 ]]; then echo "[OK] $total entr(ies) scanned — all encrypted"; else + echo "[CRITICAL] $bad of $total *.sops.yaml file(s) are NOT encrypted — fix before committing"; exit 1; fi +} + +cmd_new() { + local path="" kind="generic" name="" url="" tags=() sets=() + path="${1:?usage: new --kind --set k=v ...}"; shift + while [[ $# -gt 0 ]]; do case "$1" in + --kind) kind="$2"; shift 2;; --name) name="$2"; shift 2;; --url) url="$2"; shift 2;; + --tag) tags+=("$2"); shift 2;; --set) sets+=("$2"); shift 2;; + *) echo "[ERROR] unknown arg: $1" >&2; exit 64;; esac; done + [[ ${#sets[@]} -gt 0 ]] || { echo "[ERROR] need at least one --set key=value (the secret)" >&2; exit 64; } + local f; f=$(abspath "$path") + [[ -e "$f" ]] && { echo "[ERROR] already exists: ${f#$VAULT_DIR/} (use 'set' to update)" >&2; exit 1; } + mkdir -p "$(dirname "$f")" + # Build plaintext YAML with python (safe quoting); secrets go under credentials: + KIND="$kind" NAME="$name" URL="$url" TAGS="$(printf '%s\n' "${tags[@]:-}")" SETS="$(printf '%s\n' "${sets[@]}")" \ + "$PY" - "$f" <<'PY' +import os, sys, yaml +f=sys.argv[1] +doc={"kind":os.environ["KIND"],"name":os.environ.get("NAME","") or ""} +if os.environ.get("URL"): doc["url"]=os.environ["URL"] +doc["status"]="active" +tags=[t for t in os.environ.get("TAGS","").splitlines() if t.strip()] +if tags: doc["tags"]=tags +creds={} +for kv in os.environ.get("SETS","").splitlines(): + if not kv.strip(): continue + k,_,v=kv.partition("="); creds[k.strip()]=v +doc["credentials"]=creds +doc["notes"]="" +with open(f,"w",encoding="utf-8",newline="\n") as fh: + yaml.safe_dump(doc,fh,default_flow_style=False,sort_keys=False,allow_unicode=True) +PY + ( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] sops encrypt failed; removing plaintext" >&2; rm -f "$f"; exit 1; } + cmd_verify "$path" + echo "[INFO] Created ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh (Phase 6 commits+pushes the vault)" +} + +cmd_set() { + local path="" sets=() + path="${1:?usage: set --set k=v ...}"; shift + while [[ $# -gt 0 ]]; do case "$1" in --set) sets+=("$2"); shift 2;; *) echo "[ERROR] unknown arg: $1" >&2; exit 64;; esac; done + [[ ${#sets[@]} -gt 0 ]] || { echo "[ERROR] need at least one --set key=value" >&2; exit 64; } + local f; f=$(abspath "$path") + [[ -f "$f" ]] || { echo "[ERROR] not found: ${f#$VAULT_DIR/} (use 'new' to create)" >&2; exit 1; } + local tmp; tmp=$(mktemp) + ( cd "$VAULT_DIR" && sops -d "$f" ) > "$tmp" 2>/dev/null || { echo "[ERROR] decrypt failed" >&2; rm -f "$tmp"; exit 1; } + SETS="$(printf '%s\n' "${sets[@]}")" "$PY" - "$tmp" <<'PY' +import os,sys,yaml +f=sys.argv[1] +doc=yaml.safe_load(open(f,encoding="utf-8")) or {} +doc.setdefault("credentials",{}) +for kv in os.environ["SETS"].splitlines(): + if not kv.strip(): continue + k,_,v=kv.partition("="); doc["credentials"][k.strip()]=v +yaml.safe_dump(doc,open(f,"w",encoding="utf-8",newline="\n"),default_flow_style=False,sort_keys=False,allow_unicode=True) +PY + cp "$tmp" "$f"; rm -f "$tmp" + ( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] re-encrypt failed" >&2; exit 1; } + cmd_verify "$path" + echo "[INFO] Updated ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh" +} + +case "${1:-}" in + new) shift; cmd_new "$@" ;; + set) shift; cmd_set "$@" ;; + verify) shift; cmd_verify "$@" ;; + check) shift; cmd_check "$@" ;; + get) shift; if [[ -n "${2:-}" ]]; then bash "$VAULT_SH" get-field "$1" "$2"; else bash "$VAULT_SH" get "$1"; fi ;; + find) shift; bash "$VAULT_SH" search "$@" ;; + list) shift; bash "$VAULT_SH" list "$@" ;; + root) echo "vault root: $VAULT_DIR"; echo "resolved via: $VAULT_HOW" ;; + *) cat >&2 < --kind [--name ..] [--url ..] [--tag T].. --set k=v [--set k=v].. + set --set k=v [--set k=v].. + verify check [dir] + get [field] find list [dir] root +Secrets ALWAYS go under credentials:. Publish changes with: bash .claude/scripts/sync.sh +EOF + exit 64 ;; +esac diff --git a/clients/vons-carstar/cloud/azure.md b/clients/vons-carstar/cloud/azure.md new file mode 100644 index 0000000..2b6ed92 --- /dev/null +++ b/clients/vons-carstar/cloud/azure.md @@ -0,0 +1,28 @@ +# Azure / Cloud Services + +## Azure Subscription +- Subscription Name: +- Subscription ID: +- Resource Group(s): +- Region: +- Monthly Spend (approx): + +## Virtual Machines +| VM Name | Size | OS | IP | Purpose | +|---------------|------------|------------|------------|-----------------| +| | | | | | + +## Networking +- Virtual Network: +- Address Space: +- Subnets: +- VPN Gateway to On-Prem: Yes/No +- ExpressRoute: Yes/No + +## Other Cloud Services + +| Service | Purpose | Admin URL | Notes | +|-----------------|------------------|------------------|-----------------| +| | | | | + +## Notes diff --git a/clients/vons-carstar/cloud/m365.md b/clients/vons-carstar/cloud/m365.md new file mode 100644 index 0000000..5c970b1 --- /dev/null +++ b/clients/vons-carstar/cloud/m365.md @@ -0,0 +1,51 @@ +# Microsoft 365 + +## Tenant Info +- Tenant Name: Von's Carstar +- Tenant ID: 53de51b9-a063-4f46-88ff-7c3468828ed9 +- Primary Domain: vonscarstar.com +- Tenant Type: Managed (not federated) +- Admin Portal URL: https://admin.microsoft.com + +## ComputerGuru Management Access +- **App suite onboarded:** 2026-06-01 (Tenant Admin consented by Rob; rest auto-consented + roles assigned via `onboard-tenant.sh`). + - Tenant Admin → Conditional Access Administrator + - Security Investigator → Exchange Administrator + - Exchange Operator → Exchange Administrator + - User Manager → User Administrator + Authentication Administrator + - Defender Add-on → **incomplete** (2 ATP perms failed — no Microsoft Defender for Endpoint license; re-run onboard if MDE is added) +- **GDAP:** not required for ongoing access — the app-suite consent above gives durable, **non-expiring** admin access independent of GDAP, so the impending GDAP expiry is a non-issue. Reissue GDAP via the suite/CIPP only if delegated/portal admin is ever specifically needed. (Aside: the CIPP API client `ClaudeCipp2`/`420cb849` currently has no CIPP role — 403 on every endpoint — so CIPP-API automation is unavailable until a role is assigned; not blocking anything here.) + +## Licensing + +| License Type | Quantity | Assigned | Available | +|--------------------------------------|----------|----------|-----------| +| Exchange Online (Plan 1) — EXCHANGESTANDARD | 8 | 8 | 0 | + +Total users: **10** (8 licensed; 2 unlicensed — likely shared mailboxes / admin). + +## Exchange Online +- Mail Domain(s): vonscarstar.com +- MX Record Points To: `vonscarstar-com.mail.protection.outlook.com` (M365 / EOP, pref 0) +- **Stale secondary MX:** `mx00.1and1.com` (1&1 IONOS, pref 10) — leftover from a prior host; should be removed to avoid split/misrouted delivery. +- SPF Record: +- DKIM Enabled: +- DMARC Policy: +- Shared Mailboxes: +- Distribution Groups: +- Mail Flow Rules: + +## SharePoint / OneDrive +- External Sharing: + +## Entra ID (Azure AD) +- MFA Enforced: +- Conditional Access Policies: + +## Security +- Defender for Office 365: +- MDE (Defender for Endpoint): No (Defender Add-on onboarding failed on missing MDE license) +- Audit Log Retention: + +## Notes +- Onboarding + GDAP work: session 2026-06-01. tenants.md row = Onboarded: YES. diff --git a/clients/vons-carstar/issues/log.md b/clients/vons-carstar/issues/log.md new file mode 100644 index 0000000..8af8070 --- /dev/null +++ b/clients/vons-carstar/issues/log.md @@ -0,0 +1,19 @@ +# Issue Log + +Record past issues and their resolutions here. This helps the AI learn from historical +troubleshooting and avoid repeating failed approaches. + +## Template + +### [DATE] - [Brief Description] +- **Reported By:** +- **Severity:** Low / Medium / High / Critical +- **Symptoms:** +- **Root Cause:** +- **Resolution:** +- **Time to Resolve:** +- **Lessons Learned:** + +--- + + diff --git a/clients/vons-carstar/network/dhcp.md b/clients/vons-carstar/network/dhcp.md new file mode 100644 index 0000000..54ea0db --- /dev/null +++ b/clients/vons-carstar/network/dhcp.md @@ -0,0 +1,31 @@ +# DHCP Configuration + +## DHCP Server +- Server Name: +- Server IP: +- Failover Partner: + +## Scopes + +### Scope - [VLAN Name] +- Subnet: +- Range Start: +- Range End: +- Subnet Mask: +- Default Gateway: +- DNS Servers: +- Lease Duration: +- Exclusions: + + + +## Reservations +| Device Name | MAC Address | IP Address | Scope | Notes | +|-----------------|-------------------|-----------------|---------------|---------------| +| | | | | | + +## DHCP Relay +- Relay agents configured on: +- Helper address: + +## Notes diff --git a/clients/vons-carstar/network/dns.md b/clients/vons-carstar/network/dns.md new file mode 100644 index 0000000..7439618 --- /dev/null +++ b/clients/vons-carstar/network/dns.md @@ -0,0 +1,33 @@ +# DNS Configuration + +## Internal DNS Servers +| Server Name | IP Address | Role | +|-------------|-----------|-------------------| +| | | Primary | +| | | Secondary | + +## DNS Forwarders +- Forwarder 1: +- Forwarder 2: + +## Conditional Forwarders +| Domain | Forward To | Purpose | +|----------------------|-----------------|-------------------| +| | | | + +## Key DNS Records +| Record Type | Name | Value | Notes | +|-------------|------------------|------------------|------------------| +| A | | | | +| CNAME | | | | +| MX | | | | +| TXT | | | | + +## External DNS +- Registrar: +- Hosted At: +- Primary Domain: +- Management URL: + +## Notes + diff --git a/clients/vons-carstar/network/firewall.md b/clients/vons-carstar/network/firewall.md new file mode 100644 index 0000000..90b82fe --- /dev/null +++ b/clients/vons-carstar/network/firewall.md @@ -0,0 +1,47 @@ +# Firewall Configuration + +## Device Info +- Vendor/Model: +- Firmware Version: +- Management IP: +- Management URL: +- HA Pair: Yes/No +- License Expiry: + +## Interfaces +| Interface | Zone | IP Address | VLAN | Description | +|-----------|-----------|-----------------|------|-------------------| +| WAN1 | WAN | | | Primary Internet | +| WAN2 | WAN | | | Backup Internet | +| LAN | LAN | | | | +| DMZ | DMZ | | | | + +## NAT Rules +| Name | Source | Destination | Port(s) | NAT To | +|-------------------|---------------|----------------|-------------|-----------------| +| | | | | | + +## Key Firewall Policies +| Name | Source Zone | Dest Zone | Service | Action | Notes | +|-------------------|--------------|---------------|-------------|--------|--------| +| | | | | | | + +## VPN +### Site-to-Site VPNs +| Peer Name | Peer IP | Local Subnet | Remote Subnet | Status | +|-------------------|--------------|----------------|---------------|--------| +| | | | | | + +### SSL/Client VPN +- Enabled: Yes/No +- Portal URL: +- Auth Method: +- IP Pool: +- Split Tunnel: Yes/No + +## Content Filtering +- Web Filter Profile: +- App Control Profile: +- DNS Filter: + +## Notes diff --git a/clients/vons-carstar/network/topology.md b/clients/vons-carstar/network/topology.md new file mode 100644 index 0000000..5e8b5c7 --- /dev/null +++ b/clients/vons-carstar/network/topology.md @@ -0,0 +1,43 @@ +# Network Topology + +## Internet Connection +- ISP: +- Circuit Type: +- Speed (Down/Up): +- Public IP: +- Gateway: +- Modem Model: + +## Core Switch +- Model: +- IP Address: +- Management URL: +- Firmware Version: +- Location: + +## Additional Switches + +### Switch - [Name/Location] +- Model: +- IP Address: +- Port Count: +- PoE: Yes/No +- Uplink To: + +## Wireless +- Controller Model: +- Controller IP: +- Number of APs: +- AP Model(s): + +### Access Points + +- AP Name: +- Location: +- IP Address: +- Connected Switch/Port: + +## WAN / SD-WAN +- SD-WAN Vendor: +- Number of Sites: +- Hub Site: diff --git a/clients/vons-carstar/network/vlans.md b/clients/vons-carstar/network/vlans.md new file mode 100644 index 0000000..5b5a338 --- /dev/null +++ b/clients/vons-carstar/network/vlans.md @@ -0,0 +1,21 @@ +# VLANs + +## VLAN Table + +| VLAN ID | Name | Subnet | Gateway | DHCP Scope | Purpose | +|---------|---------------|-----------------|-----------------|------------------|------------------------| +| 1 | Default | | | | | +| 10 | Management | | | | Network devices | +| 20 | Servers | | | | Server infrastructure | +| 30 | Workstations | | | | End user devices | +| 40 | VoIP | | | | Phone system | +| 50 | WiFi-Corp | | | | Corporate wireless | +| 60 | WiFi-Guest | | | | Guest wireless | +| 100 | Security | | | | Cameras / access ctrl | + +## Inter-VLAN Routing +- Performed by: +- Routing device IP: + +## VLAN Notes + diff --git a/clients/vons-carstar/overview.md b/clients/vons-carstar/overview.md new file mode 100644 index 0000000..b226b90 --- /dev/null +++ b/clients/vons-carstar/overview.md @@ -0,0 +1,34 @@ +# Client Overview + +## Company Name +Von's Carstar (Carstar collision-repair / auto body franchise) + +## Primary Contact +- Name: +- Phone: +- Email: + +## IT Contact +- Name: Rob +- Phone: +- Email: + +## Contract Details +- Service Level: +- Hours Covered: +- Contract Renewal Date: + +## Environment Summary +- Total Users: 10 (8 licensed — Exchange Online Plan 1) +- Total Locations: +- Domain Name: vonscarstar.com +- Primary Site Address: +- RMM Agent Count: +- Workstation Count: +- Server Count: + +## Notes +- Microsoft 365 client. Tenant `53de51b9-a063-4f46-88ff-7c3468828ed9`. +- **Onboarded to the ComputerGuru M365 app suite 2026-06-01** (Tenant Admin consented by Rob; full suite roles assigned — see `cloud/m365.md`). +- **GDAP not required** — the app-suite consent provides durable, non-expiring admin access, so the impending GDAP expiry is moot. Reissue via the suite only if delegated/portal admin is ever specifically needed. +- User/license/site detail still TBD — pull via the remediation tool now that the tenant is onboarded. diff --git a/clients/vons-carstar/rmm/rmm.md b/clients/vons-carstar/rmm/rmm.md new file mode 100644 index 0000000..feea500 --- /dev/null +++ b/clients/vons-carstar/rmm/rmm.md @@ -0,0 +1,34 @@ +# RMM / Monitoring + +## RMM Solution +- Product: +- Console URL: +- Agent Version: + +## Agent Deployment +- Total Devices: +- Servers Monitored: +- Workstations Monitored: +- Network Devices Monitored: + +## Monitoring Policies +| Policy Name | Applies To | Alert Condition | Action | +|-------------------|----------------|-------------------------|---------------| +| Disk Space | All Servers | < 10% free | Alert + Ticket| +| CPU | All Servers | > 90% for 15 min | Alert | +| Service Monitor | All Servers | | | +| Backup Monitor | | | | +| Offline Alert | All Agents | Offline > 30 min | Alert | + +## Patch Management +- Patch Policy: +- Patch Window: +- Auto-approve: Yes/No +- Exclusions: + +## Scripting / Automation +| Script Name | Schedule | Purpose | +|---------------------|-------------|--------------------------| +| | | | + +## Notes diff --git a/clients/vons-carstar/security/antivirus.md b/clients/vons-carstar/security/antivirus.md new file mode 100644 index 0000000..786f4ec --- /dev/null +++ b/clients/vons-carstar/security/antivirus.md @@ -0,0 +1,26 @@ +# Endpoint Security / Antivirus + +## Solution +- Product: +- Console URL: +- License Count: +- License Expiry: +- Managed By: + +## Policy +- Real-time Protection: Yes/No +- Scheduled Scans: (frequency) +- Exclusions: + +## Deployment Status +- Total Endpoints: +- Protected: +- Missing Agent: +- Out of Date: + +## EDR / XDR +- EDR Enabled: Yes/No +- Product: +- Console URL: + +## Notes diff --git a/clients/vons-carstar/security/backup.md b/clients/vons-carstar/security/backup.md new file mode 100644 index 0000000..a0e6ef1 --- /dev/null +++ b/clients/vons-carstar/security/backup.md @@ -0,0 +1,34 @@ +# Backup and Disaster Recovery + +## Backup Solution +- Product: +- Console URL: +- License/Subscription: + +## Backup Targets +| Target Name | Type | Location | Capacity | Encrypted | +|----------------|----------------|-----------------|--------------|-----------| +| | Local NAS | | | Yes/No | +| | Cloud | | | Yes/No | +| | Offsite | | | Yes/No | + +## Backup Jobs +| Job Name | Source | Target | Schedule | Retention | Status | +|-----------------|-------------------|------------|---------------|-------------|--------| +| | | | | | | + +## M365 Backup +- M365 Backup Product: +- Exchange Backed Up: Yes/No +- SharePoint Backed Up: Yes/No +- OneDrive Backed Up: Yes/No +- Teams Backed Up: Yes/No + +## Disaster Recovery Plan +- RTO Target: +- RPO Target: +- DR Site: +- Last DR Test Date: +- DR Test Result: + +## Notes diff --git a/clients/vons-carstar/servers/server_template.md b/clients/vons-carstar/servers/server_template.md new file mode 100644 index 0000000..dfaf7b0 --- /dev/null +++ b/clients/vons-carstar/servers/server_template.md @@ -0,0 +1,49 @@ +# Server: [SERVER NAME] + +## General Info +- Hostname: +- IP Address: +- OS: +- OS Version: +- Physical / Virtual: +- Host (if virtual): +- Location: +- Last Patched: + +## Hardware (if physical) +- Make/Model: +- CPU: +- RAM: +- Storage: +- Warranty Expiry: + +## Roles and Services + +- [ ] Domain Controller +- [ ] DNS Server +- [ ] DHCP Server +- [ ] File Server +- [ ] Print Server +- [ ] Application Server +- [ ] Database Server +- [ ] Backup Target +- [ ] RDS / Terminal Server +- [ ] Hyper-V Host + +## Shares (if file server) +| Share Name | Path | Permissions Group | Notes | +|---------------|-------------------|---------------------|----------------| +| | | | | + +## Applications Installed +| Application | Version | Purpose | License | +|-------------------|------------|----------------------|---------------| +| | | | | + +## Backup +- Backup Method: +- Backup Schedule: +- Backup Target: +- Last Verified Restore: + +## Notes