Compare commits

..
20 Commits
Author SHA1 Message Date
mrcharles aeb2e80ab1 fix(console): enable conmand remote access + add conman client script
conmand was binding to localhost only (server loopback=on default), so
the conman client on workstations couldn't connect. The intended workflow
is: conman client (workstation) → conmand (pfv-tsys4:7890 over Tailscale)
→ ser2net (TCP 2001-2007) → serial device. Without remote conmand access,
users had to telnet directly to ser2net, which conflicts with conmand's
persistent connections (kickolduser kicks the telnet session immediately).

Changes:
- generate-config.sh: add server loopback=off to conman.conf so conmand
  listens on 0.0.0.0:7890 (reachable via Tailscale)
- query-remote.sh: new script for workstations — installs conman client,
  verifies connectivity, lists or connects to consoles
- README.md: clarify access model (conman primary, telnet emergency only
  with conmand stopped). Document the kickolduser conflict.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 20:07:17 -05:00
mrcharles 21adb89d4e docs(audit): fresh fleet audit + fix stale paths across 13 perf scripts
Fresh Proxmox fleet audit (2026-07-28) with current VM placements, RAM,
CPU, and storage for all 7 reachable hosts. Written to
docs/proxmox/AUDIT-2026-07-28.md — supersedes placement data in
PROJECT.md sections 4-8.

Key audit findings:
- CRITICAL: UCS01/02 and netinfra01/02 HA pairs both still on tsys4
  storage. tsys4 failure = DNS/DHCP/NTP + LDAP/AD fully dark. These
  migrations were the #1 recommendation from the previous audit and
  have not been done.
- CRITICAL: 2 of 3 active k3s cnodes (cnode1 + cnode2) on tsys4 NFS.
  tsys4 failure = etcd quorum lost.
- 59% of running VMs still on tsys4 storage (improved from 68%).
- cnode VMIDs have changed since PROJECT.md was written (cnode1 is now
  VMID 906 on tsys9, cnode2 is VMID 705 on tsys7, etc.)

Gardening fixes:
- Removed duplicate fleet-audit.sh (check.sh + deploy-check.sh already
  exist for this purpose)
- Fixed hardcoded path /home/reachableceo/projects/perfopt in 13 perf/
  scripts to use BASH_SOURCE-derived relative paths (per AGENTS.md
  self-locating scripts convention)
- Updated STATUS.md Known Issues with the two critical findings
- Updated STATUS.md Pending with prioritized pre-k8s action items
- Registered AUDIT-2026-07-28.md in docmap.md

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 20:07:09 -05:00
mrcharles 5ce5d2e6b7 fix(powerman): use -h flag for remote server in query script
Debian's powerman client uses the -h/--server-host flag, not the
POWERMAN_SERVER env var, to connect to a remote daemon. Update query-remote.sh
to pass -h explicitly.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:48:02 -05:00
mrcharles fff36e0bb2 feat(powerman): add remote query script for workstation PDU access
Script for any Tailscale-connected workstation to install the powerman
client and query the Cyclades PDU on pfv-tsys1. Sets POWERMAN_SERVER so
all powerman commands route to the remote daemon. Handles missing sudo
gracefully with instructions for manual install.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:47:01 -05:00
mrcharles a980a4df2f fix(powerman): bind to Tailscale + localhost instead of 0.0.0.0
Change powermand listen address from 0.0.0.0:10101 (all interfaces) to
127.0.0.1:10101 (local admin) + Tailscale IP:10101 (remote access). The
setup.sh now auto-detects the Tailscale IP at deploy time.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:45:17 -05:00
mrcharles 815d07bbca feat(console): manage 7 switch consoles via ser2net+conman on pfv-tsys4
Solve the long-standing USB adapter enumeration shift problem: 9 Prolific
USB-to-DB9 adapters on pfv-tsys4 have no unique serial numbers and get
assigned /dev/ttyUSB0-8 based on enumeration order, which changes on every
reboot and breaks the old /root/conmap + manual screen workflow.

Solution: udev rules pin each adapter by its ID_PATH (physical USB port
topology), which is stable across reboots regardless of enumeration order.
Each adapter gets a named symlink in /dev/consoles/<name>. ser2net opens
these stable symlinks and exposes them on TCP ports (2001-2007) bound to
the Tailscale interface only. conman connects to those TCP ports for
session logging and multi-user console sharing.

Architecture (layered, no port sharing):
  USB adapter → udev symlink → ser2net (TCP) → conman (logging + mux)

Port assignments (all on Tailscale IP 100.70.77.93):
  2001 = pfv-core-sw01     2002 = pfv-tor3-mgmt    2003 = pfv-tor3-stor
  2004 = pfv-rrinfra-rtr   2005 = pfv-r2-tor-top   2006 = subodev-torsw
  2007 = pfv-r2-sw

Scripts (console/):
- mapping.txt: source of truth (TCP port | name | ID_PATH | baud | comment)
- generate-config.sh: generates udev rules, ser2net.yaml, conman.conf
  entries from mapping.txt. Idempotent (markers in conman.conf for clean
  regeneration). Uses | delimiter (ID_PATH values contain colons).
- setup.sh: full deploy — generate configs, create symlinks (udev trigger
  + manual fallback for already-discovered devices), create conmand
  systemd unit (Debian doesn't ship one), restart services
- discover.sh: read-only USB adapter and service state discovery
- validate-conman.sh: verify conman→ser2net→device data path and log capture

Issues fixed during development:
- /dev/console is a kernel char device (major 5, minor 1) — cannot create
  a directory there. Changed symlink namespace to /dev/consoles/.
- conman 0.3.x has no 'include' directive — CONSOLE entries written
  directly into /etc/conman.conf between idempotent markers.
- Debian conman package has no systemd unit — created
  /etc/systemd/system/conmand.service with After=ser2net ordering.
- conman.conf had no LOGDIR — logs weren't being written to
  /var/log/conman/. Fixed by adding server logdir directive.

Validation: 7 symlinks resolving, 7 TCP ports on Tailscale, conmand with
7 consoles registered, 7 log files actively capturing console output,
both services enabled for reboot survival.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:44:02 -05:00
mrcharles e8969b5d12 docs: fix gardening variance + strengthen protocol for new directories
Two variances from the gardening protocol were found during self-audit:
the k8s/ and powerman/ directories were added but the root README.md
directory table and the AGENTS.md Key Scripts table were not updated.

Root cause: the gardening protocol enumerated STATUS.md and docmap.md but
did not explicitly call out the root README.md directory table or the
AGENTS.md Key Scripts table, so they were easy to miss when adding a new
top-level directory.

Fixes:
- README.md: add k8s/ and powerman/ to the Directory Structure table
- AGENTS.md: add install-cp.sh and powerman/setup.sh to Key Scripts

Permanent fix (encode in protocol so it cannot recur):
- Add rule 5 to the Automatic Gardening Protocol: when a new top-level
  directory is created, ALL directory listings must be updated
  (README.md table, AGENTS.md layout block, AGENTS.md Key Scripts)
- Add rule 6: a grep-based self-audit command to run before commit,
  verifying the new directory appears in all four canonical files

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 18:49:39 -05:00
mrcharles a5eabf7692 feat(powerman): manage Cyclades PM10i PDU via powerman on pfv-tsys1
Set up centralized PDU management for a Cyclades AlterPath PM10i (10
controllable AC outlets) connected to pfv-tsys1 via a Prolific USB-to-DB9
serial adapter. powermand is now listening on 0.0.0.0:10101, making the
PDU manageable over the network from any host on the tailnet.

Scripts (powerman/):
- discover.sh: gather USB adapter, powerman state, device definitions
- setup.sh: idempotent setup — udev rule (stable symlink by serial number),
  powerman.conf with 10 outlet nodes, fix powermand dialout group, restart
  service. Overridable via env vars for other hosts/PDU types
- test-pdu.sh: validate control by cycling outlet 10 off then on (8/8 pass)
- status.sh: quick PDU status check

Issues fixed during setup:
- Config pointed at /dev/ttyUSB0 but adapter is at /dev/ttyUSB1 (fixed
  with udev symlink /dev/cyclades-pm10 pinned to adapter serial)
- powermand (user:powerman) lacked dialout group membership to open the
  serial device (fixed with usermod + udev GROUP="dialout")

Validation: outlet 10 turned off (confirmed), turned on (confirmed), then
cycled. All 10 outlets currently ON and manageable.

TODO tracked for Friday: rename outlets from generic (outlet-1..10) to
match physical devices, and change PDU admin password from factory default.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 18:22:47 -05:00
mrcharles e893fc80e9 feat(k8s): deploy 3-node k3s HA control plane over Tailscale
Bootstrap a regular (non-ITAR) k3s cluster on cnode1/2/3 with embedded
etcd. All cluster communication — node registration, API server, etcd
peering, flannel VXLAN — runs exclusively over Tailscale IPs. Zero LAN
addresses appear in node status or TLS certificates.

Scripts (k8s/):
- env.sh: shared config (Tailscale IPs, SSH opts, k3s version)
- wipe.sh: remove existing k3s from all cnodes
- install-cp.sh: full bootstrap (cnode1 --cluster-init, then cnode2/3 join)
- join-servers.sh: re-join cnode2/3 only (fixes broken join state)
- post-setup.sh: apply NoSchedule taints, fetch kubeconfig, verify
- verify.sh: 13-point health check (nodes, Tailscale IPs, taints, etcd,
  CoreDNS, API server, workload isolation)
- probe-nodes.sh: SSH + Tailscale reachability check

All 3 cnodes are tainted control-plane:NoSchedule so no user workloads
can schedule on the control plane. 13/13 health checks pass.

Docs updated: k8s README TL;DR reflects k3s (not Talos) as the deployed
choice, with Talos preserved for the future ITAR cluster.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 12:21:33 -05:00
mrcharles 2d01a9f6be docs(k8s): add Talos architecture, distro decision, and bootstrap plan
Author the docs/k8s/ directory capturing the pfv-k8s control-plane design:

- README.md: TL;DR of all decisions (distro, runtime, cnode count, admin
  access, identity, tenancy, registry, storage)
- DISTRO-DECISION.md: Talos vs k3s analysis. Recommend Talos because the
  ITAR/classified requirement makes its immutable, API-only, measured-boot
  posture structurally easier to certify than SCAP-hardened Debian. k3s was
  only ever a plan (no cluster deployed), so cutover cost is ~zero.
- ARCHITECTURE.md: target arch with mermaid diagrams covering control
  plane, LAN-only network with Tailscale subnet-router admin, Cilium CNI,
  OIDC to Keycloak, per-tenant vcluster isolation (incl. ITAR tenant),
  Harbor pull-through cache on D3 SSD, bootstrap sequence, and DR.

Gardening: register docs/k8s/ in docmap.md, update STATUS.md with the new
k8s section and the three pending user decisions (cnode count, host spread,
Tailscale pattern).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 11:50:56 -05:00
mrcharles d65a5fa34c docs(agents): enforce always-commit-push policy
Strengthen Git Policy point 1 so agents never hold work for review. The
user reviews rendered markdown on Gitea after push, so pausing to "let
them read first" defeats the workflow. Explicitly overrides any default
conservative commit-and-hold behavior.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 11:50:51 -05:00
mrcharles 6dc8c51580 docs: organize docs into project subdirectories
Split docs/ into project-based subdirectories:
- docs/proxmox/     fleet ops, hardware, k8s (PROJECT.md, TODO.md, K8S.md)
- docs/server-build/ provisioning, security, DNS (SECURITY.md, tailscale.md,
                    DEPLOYMENT.md, TSYS-2FA-GUIDE.md, DEVELOPMENT-GUIDELINES.md)
- docs/archive/     historical AI reviews, completed todos, pre-refactor docs

docmap.md rewritten with new paths. All cross-references in AGENTS.md,
README.md, STATUS.md, and dns-cluster-setup/README.md updated.

Code directories unchanged — scripts stay where BASH_SOURCE expects them.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:34:23 -05:00
mrcharles 3d5b6c859e docs: add STATUS.md, docmap.md, encode gardening protocol
Restructure top-level to exactly three .md files:
- AGENTS.md (agent operating instructions)
- README.md (project overview, links to status + docmap)
- STATUS.md (living project status, agent-maintained, human read-only)

Add docs/docmap.md as the single documentation index/map. All docs are
categorized (active, operational guides, historical) with last-reviewed
dates. Includes the agent gardening protocol requiring agents to update
STATUS.md and docmap.md after every work session.

Rewrite AGENTS.md to be lean: points to docmap.md for doc discovery,
encodes the automatic gardening protocol (keep docs/code in sync, grep
for stale paths after renames, update STATUS.md after infrastructure
changes). All references are Gitea-renderable relative links.

Simplify README.md: header links to STATUS.md + docmap.md + AGENTS.md,
doc table replaced with pointer to docmap.md.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:32:03 -05:00
mrcharles 37e59ca310 docs: end-to-end gardening — links, stale refs, tailscale.md update
Comprehensive documentation gardening across the merged repo:

- tailscale.md: fully rewritten with current ground truth. The netinfra
  pair now runs production Technitium with all knel.net records
  replicated. Both LAN IPs resolve knel.net device names and recurse
  externally. The old "NXDOMAIN / zone is stale" findings are replaced
  with the resolved state and current recommendations.
- AGENTS.md: rewritten with Gitea-compatible clickable relative links
  to all key scripts and docs. Autonomous commit/push policy
  prominently documented. SSH user corrected to localuser.
- README.md: directory table and docs table now use clickable links.
- All .md cross-references converted to Gitea-renderable relative links.
- Stale path references (ProjectCode/, Project-Tests/, ProjectDocs/)
  updated to current names (provisioning/, tests/) across all docs.
- Stale repo name "FetchApply" / "KNELServerBuild" updated to
  "PFVCluster" in actionable docs; historical AI-review docs tagged
  with an HTML comment notice.
- REFACTORING-EXAMPLES.md: tagged as historical (pre-refactor patterns).
- tests/README.md, dns-cluster-setup/README.md, docs/DEPLOYMENT.md,
  docs/SECURITY.md: path references fixed to current structure.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:28:52 -05:00
mrcharles 8e5b9558fe docs: unified README and AGENTS.md for merged repo
Replace the KNELServerBuild README with a unified PFVCluster README
covering both provisioning and cluster ops. Update AGENTS.md to document
the merged repo layout, key scripts, and project context. Consolidate
all documentation under docs/.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:25:16 -05:00
mrcharles 66e7843f27 refactor: reorganize merged repo into clean directory structure
Reorganize the merged KNELServerBuild + PFVCluster repo:

  provisioning/    server provisioning (was ProjectCode/ +
                   Project-Includes/ + Project-ConfigFiles/)
  tests/           test suite (was Project-Tests/)
  perf/            Proxmox perf scripts (was top-level *.sh + scripts/)
  docs/            all documentation (was ProjectDocs/ + PROJECT.md +
                   K8S.md + TODO.md)
  dns-cluster-setup/  Technitium DNS cluster (unchanged)
  netinfra/        netinfra audit scripts (unchanged)
  switches/        switch configs (unchanged)
  vendor/          vendored KNELShellFramework (unchanged)

Update all internal path references from old directory names
(ProjectCode/, Project-Includes/, Project-Tests/) to the new ones
(provisioning/, tests/) across all scripts.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:24:39 -05:00
mrcharles c14b48f39e merge: combine KNELServerBuild into PFVCluster
Merge the KNELServerBuild repository (server provisioning, security
hardening, DNS cluster setup, test suite) into PFVCluster (Proxmox
cluster ops, performance tuning, fleet audit). Both histories are
preserved via --allow-unrelated-histories.

The two repos had no source-file collisions; only AGENTS.md and
.gitignore conflicted (both resolved by merging content from both).

Directory reorganization and doc gardening will follow in subsequent
commits.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:23:22 -05:00
mrcharles 18d57ea4fb feat: wire Pi-hole to forward knel.net to local Technitium + document setup
Add a shared Docker network (dnsnet, 10.53.0.0/24) connecting Pi-hole and
Technitium containers so Pi-hole can conditionally forward knel.net and
Tailscale-reverse queries to the local authoritative Technitium instance
(10.53.0.53) instead of netboot's upstream 192.168.3.16. Also adds
end-to-end documentation for both the reference node (pfv-netboot) and
the replicated nodes (pfv-netinfra-01/02).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 05:16:01 -05:00
mrcharles 89469ff028 feat: full re-audit of all 7 hosts with fresh ground truth
Deployed check.sh to all 7 hosts at 21:50 CDT. Captures the live state
after the user's PDM migrations:

Cnode movements since last audit:
- cnode1: tsys1 -> tsys9
- cnode2: tsys6 -> tsys7
- cnode5: tsys6 -> tsys7, storage D5(tsys4) -> S2(tsys5)

Wnode changes:
- wnode-tsys1 (102): new VM on S2, stopped
- wnode-tsys3: RAM bumped 20 -> 28 GB
- wnode-tsys6: now running (was stopped)
- wnode-tsys9: storage moved S3 -> S2

Storage distribution improved from 90/10 to 73/27 (tsys4/tsys5).
Still need 2 more cnode moves for etcd quorum survival.

Updated executive summary, k8s distribution tables, storage
utilization, and open items with the fresh data. Captured future k8s
requirements: vcluster + Rancher, OIDC to Keycloak, workload isolation
(RackRental/Suborbital ITAR/non-ITAR/SLP), and solar-aware scale-out
with PowerEdge 19xx/2950 systems.

Added tsys9 to deploy-check.sh host list.
2026-07-27 22:04:05 -05:00
mrcharles 48cb6842c6 docs: lock in storage philosophy and PDM migration capability
Storage philosophy (user directive):
- NVMe/SSD: k8s worker scratch + ultix-streaming (dev workstation
  running "cluster of 1" pre-prod jobs before full k8s deployment)
- Spinning rust: all infrastructure VMs (UCS, netinfra, LibreNMS, SIEM)

Clarified that hosts are standalone but managed via Proxmox Datacenter
Manager (PDM), which supports VM migration between nodes through the
UI -- eliminating the need for manual disk copies in the migration plan.

Updated all migration steps to reference PDM storage migrate instead
of manual cp commands.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-27 21:46:48 -05:00
292 changed files with 28154 additions and 23631 deletions
-3
View File
@@ -1,3 +0,0 @@
*
!crush.json
!.gitignore
-11
View File
@@ -1,11 +0,0 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(bash|edit|write|multiedit|lsp_replace_symbol|lsp_rename)$",
"command": "./hooks/ticket-gate.sh",
"timeout": 5
}
]
}
}
-7
View File
@@ -1,7 +0,0 @@
# PFVCluster environment variables
# Copy to .env and fill in values for local development/testing.
# Pi-hole (netinfra/pihole/docker-compose.yml)
PIHOLE_WEB_PASSWORD=changeme
# Shellcheck wrapper (tests/shellcheck.sh) — no config needed, uses Docker.
+2 -9
View File
@@ -1,9 +1,5 @@
# Crush internal state (track crush.json config, ignore session data)
.crush/crush.db
.crush/crush.db-*
.crush/logs/
.crush/memory/
.crush/active-ticket
# Crush internal state
.crush/
# OS/editor
.DS_Store
@@ -41,6 +37,3 @@ returned-logs/
# Kubernetes secrets (kubeconfig contains embedded client certs)
k8s/kubeconfig.yaml
k8s/*.token
# Pi-hole web UI password (real value only in on-box .env, never committed)
netinfra/pihole/.env
-17
View File
@@ -1,17 +0,0 @@
# ShellCheck configuration for PFVCluster
# (used when running `shellcheck` directly; tests/shellcheck.sh applies the
# same disables via -e for consistent results under Docker)
#
# These checks are DISABLED because they flag intentional conventions of this
# codebase, not bugs:
#
# SC1090 / SC1091 — cannot follow dynamically-computed `source` paths. The KNEL
# framework (vendor/) and test harness source helpers via computed include
# dirs, which shellcheck cannot resolve statically.
# SC2029 — ssh orchestration (tests/remote.sh and perf/k8s/dns scripts)
# deliberately builds and expands the remote command on the CLIENT side before
# sending it. That is the whole point of the single-chokepoint remote pattern.
disable=SC1090,SC1091,SC2029
# Treat external-sourced files as bash (matches #!/usr/bin/env bash framework).
external-sources=true
+59 -549
View File
@@ -1,311 +1,37 @@
# Agent Guidelines
## Agent Authority (NON-NEGOTIABLE)
**No work is permissible on any system without an approved Redmine ticket.
There are no exceptions to this rule.**
### Scope of authority
1. **Tickets govern all work.** The agent performs ONLY the work described in
the approved Redmine ticket. Anything outside that scope — no matter how
small, helpful, or "obvious" — is prohibited.
2. **No autonomous system changes.** The agent does not modify, configure,
create, delete, or grant anything on a production system unless it is
explicitly directed by an approved ticket. "Production system" means
every system in the fleet — there is no "test" exception unless the ticket
says so.
3. **Propose, never implement.** If the agent discovers additional work that
should be done — a bug, a misconfiguration, a missing dependency, an
enhancement — it does NOT implement it. Instead, it creates a Redmine
sub-ticket (status Feedback) describing the finding and surfaces it to
the user for approval.
4. **Security and access changes require extra scrutiny.** Changes to sudoers,
SSH keys, user accounts, firewall rules, authentication policy, file
permissions, or any privilege-related configuration are treated as
policy decisions, not implementation details. The agent may suggest
them but NEVER implements them without explicit user direction in the
ticket or a sub-ticket the user has approved.
5. **The user makes policy. The agent implements policy.** The agent does
not decide who gets sudo, what keys go where, what services run, or what
the access model is. The agent executes the user's decisions, exactly
as specified.
6. **When in doubt, ask.** If the ticket is ambiguous, if a task seems to
require something not explicitly authorized, or if the agent is unsure
whether an action is in scope — STOP and ask the user via the ticket
or directly. Asking is always acceptable. Overstepping is never
acceptable.
7. **NEVER close a ticket without explicit user permission.** You may
SUGGEST a close when the result is clearly scoped and delivered. If
it's ambiguous whether the work is truly complete, don't suggest a
close — leave that decision to the user. This applies to ALL tickets,
no exceptions.
8. **User acceptance testing is MANDATORY before declaring work done.**
The agent performs implementation and technical validation (services
running, configs correct, APIs responding). The user performs UAT —
visually confirming dashboards render data, alerts deliver, tools are
usable. The agent MUST NOT set done-ratio to 100%, MUST NOT suggest
closing, and MUST NOT move to the next ticket until the user explicitly
accepts the work. "Technically wired but blank dashboard" is NOT done.
9. **NEVER access a database directly if an API exists.** APIs are the
stable contract; databases change schemas without warning. If a tool
has an API, use it — exhaust all API endpoints, check the docs, try
alternative methods. Only fall back to direct DB access as a last
resort AND with explicit user approval for that specific instance.
Soon all DB access will route through a proxy under zero trust;
building API-first habits now ensures that transition is clean.
### Access-channel policy: SSH only (NON-NEGOTIABLE)
**The qemu guest-agent is NEVER an access, execution, or key-delivery
channel.** SSH (`sshd`) is the only approved remote access path. Every
command must flow through sshd so it is captured by the standard
auth/logging/audit infrastructure. This is an ITAR/CMMC/TS/SCI
environment — there is no back-door exception, ever.
- **Forbidden:** `qm guest exec` (runs arbitrary commands inside a guest
over an unaudited channel) and any wrapper around it (e.g. a `vm-guest`
mode). This includes using guest-agent to *deliver* an SSH key, even if
the resulting SSH login is itself audited — the delivery bypassed audit.
- **Allowed:** installing or checking qemu-guest-agent for its intended
purpose — letting Proxmox see guest state (`qm guest cmd <id> ping`,
`agent: 1` config, `apt install qemu-guest-agent`). Visibility only;
never execution.
- **Enforced mechanically:** `scripts/check-rules.sh` rule #11 fails on
any `qm guest exec` / `vm-guest` pattern in code. `tests/remote.sh` has
no guest-exec mode.
- **If a system is locked out** (no SSH key, no guest-exec path): surface
it to the user. Do NOT improvise an alternate back-channel. The user
authorizes the unblock method (console login, credential, etc.).
### Ticket-closing policy (NON-NEGOTIABLE)
**NEVER close a Redmine ticket without explicit user permission.** You
may SUGGEST a close when the result is clearly scoped and delivered. If
it's ambiguous whether the work is truly complete, don't suggest a close
— leave that decision to the user. This applies to ALL tickets, no
exceptions.
### What this means in practice
- Discovered a typo in a config during approved work? **Finish the approved
work. Create a sub-ticket for the typo. Do not fix it inline.**
- Think a system should also have localuser sudo configured? **Do not add
it. Propose it in a sub-ticket.**
- Need to install a package the ticket didn't mention? **Ask first.**
- Found a security issue? **Create a ticket immediately with full details.
Do not remediate without approval.**
This environment operates in ITAR/CMMC/TS/SCI space. Every action must be
traceable to an approved ticket. There is no "I thought it would help."
## Quick Start
**You are an AI agent working on this project. Your first actions, in order:**
> **SESSION-START GATE (NON-NEGOTIABLE):** Steps 1-3 orient you. Step 4 is the
> check-for-understanding gate (rule summary). Steps 5-7 gather state. Step 8 is
> the scope-alignment gate. You MUST NOT begin any task work until the user
> (a) confirms your rule summary AND (b) names the ticket to work on. This runs
> **every session, automatically** — the user should never have to ask for it.
1. **Set up the environment:** `bash scripts/setup-hooks.sh` (installs git hooks — idempotent).
2. **Read this file** (`AGENTS.md`) — project policy and domain knowledge.
3. **Read the latest questions file** (`questions-v*.md`) — open questions awaiting human input. The version number increments each round (v1, v2, v3...).
4. **Check for understanding — GATE.** In your own words, summarize ALL the rules
back to the user before doing any work: Agent Authority (ticket-governed,
propose-never-implement), Access-Channel SSH-only policy, Remote access
(remote.sh chokepoint mandatory, DNS names only — never IP literals),
Questions policy (no harness question-tools), Documentation policy
(Discourse is SoR), Redmine tracking, Git policy (always commit+push,
shellcheck), Mandatory infra-change documentation (#298 audit log), SSH
routing chokepoints (`tests/remote.sh`), the mechanically enforced
`check-rules.sh` rules, and Credentials (Vault migration TODO). Then stop
and wait for the user to confirm. This checkpoint guarantees every
session starts aligned.
5. **Check Redmine**`docker run --rm --env-file ~/.creds/redmine.env git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55` for active work.
6. **Check current state:** `git log --oneline -10`.
7. **Run rule audit:** `bash scripts/check-rules.sh --fast`.
8. **Scope-alignment — GATE.** Present the session handoff's "What's Left"
priority list (or the Redmine queue if no handoff exists). Do NOT scan the
full ticket queue and pick work on your own — **the user directs what gets
worked on, always.** Note any new or urgent items from the Redmine check,
then ask which ticket to work on tonight. **STOP and wait.** Do not set
`.crush/active-ticket`, do not read systems, do not run diagnostics, until
the user names the target. This gate prevents the agent from burning
context on work the user didn't ask for.
## Enforcement Model
Git hooks (`scripts/pre-commit`, `scripts/pre-push`) enforce the rules defined in
`scripts/check-rules.sh`. The rules engine checks: shellcheck (zero warnings
including info-level), Docker image pinning (no `:latest`), container naming,
required files, Discourse pointer headers, and more. Run `bash scripts/check-rules.sh`
for a full audit or `--fast` for pre-commit speed. Bypass with `--no-verify`
(emergencies only).
## Task Tracking
- **Redmine is the system of record for all work.**
- **NEVER close a ticket without explicit user permission.** Suggest a
close when clearly scoped/delivered; if ambiguous, don't suggest.
- **Ticket-first enforcement (mechanically enforced).** Before starting
any work, set the active ticket: `echo '#NNN' > .crush/active-ticket`.
The Crush hook (`hooks/ticket-gate.sh`) blocks modifying operations
until this file exists. If no ticket exists, CREATE ONE FIRST via
redmine-cli, then set it. Clear when done: `> .crush/active-ticket`.
- **WORKING.md** is the only in-repo task tracker — a scratchpad for the current
session. The pre-commit hook blocks commits while any task remains unchecked.
- Clear WORKING.md before responding to the user.
## Rolling HUD (session-scoped, NOT persisted)
The rolling HUD is a **live status display** the agent maintains throughout
the session to help the human follow along. It is NOT a system of record —
Redmine, Discourse, and git are the durable systems. The HUD exists purely
for the human's situational awareness during the session.
- **Format:** a compact block shown at the end of each significant response
(after completing a step, hitting a blocker, or pivoting). Example:
```
┌─ SESSION HUD ────────────────────────────────────────
│ Active: #343 (Monitoring coverage matrix)
│ Done: ✓ scope-alignment gate added to both AGENTS.md
│ ✓ committed + pushed (6a2550b)
│ Now: drafting coverage matrix on Discourse
│ Next: → #341 TEMPer USB (deploy on pfv-tsys1)
│ → #338 LibreNMS alerts (BLOCKED: pushover.env)
├─ USER ACTION ITEMS ──────────────────────────────────
│ • Populate ~/.creds/pushover.env (unblocks #338, #428)
│ • Populate ~/.creds/prometheus.env + grafana.env (#430)
└──────────────────────────────────────────────────────
```
- **Placement:** may be written to `.crush/hud.md` on disk to keep context
window smaller (re-read and update rather than hold in memory). Never
committed to git. Wiped at session end.
- **Sections:**
- **Active:** current ticket number + one-line description
- **Done:** ✓ items completed this session (append as work progresses)
- **Now:** what the agent is actively doing
- **Next:** the queued items (per handoff priority or user direction)
- **User action items:** things ONLY the user can do (populate creds,
physical work, manual deploys) with the tickets they unblock
- **When to show it:** after each logical unit of work, at blockers, and
when pivoting between tickets. Not every trivial response — use judgment.
- **NOT a substitute for Redmine/Discourse/git.** The HUD is ephemeral. When
work completes, update the durable systems (ticket notes, Discourse wiki,
commits). The HUD just tracks the live narrative for the human.
## Working Style
- **Stop over-thinking.** Get to code and output faster. Explore with code;
gather ground truth. Do not burn tokens reasoning about things a quick command
answers.
- **Farm work out to deterministic tooling:** linters, LSPs, formatters, test
runners. If an LSP is wired up, use it; otherwise pull a Docker image and lint
inside it.
- **Use sub-agents as subcontractors:** scoped spec in, distilled deliverable out.
Never read 10+ files sequentially; batch into agent calls.
- **Command timeouts (NON-NEGOTIABLE):** Every command that touches a remote
system MUST be wrapped with `timeout`. Hard limits: 30s for quick reads
(status, ps, ls), 120s for standard operations, 300s for deployments/pulls.
If a command hits the timeout, STOP and investigate root cause — never
blindly retry. A hung command is a failed command. Detect failure fast,
diagnose, fix, move on. Example: `timeout 120 bash tests/remote.sh vm 'cmd'`.
This applies to ALL tools — bash, docker, CLIs, sub-agents.
## Questions (NON-NEGOTIABLE)
**NEVER use a harness "question"/"ask user" tool** (structured prompts,
modal forms, tabbed questions). Banned across every project, every harness.
They are not portable, not version-controlled, and bypass the git record.
**All questions go in the current `questions-v(N).md` file** — write the
question; the human edits the answer inline in the same file. **Version up
the filename each time answers land** (v1 → v2 → v3...): create
`questions-v2.md` with resolved Q&A marked, new questions appended. This
preserves the history of each Q&A round. Synthesize resolved Q&A into
Discourse (decisions) and Redmine (work items). See `BASELINE-PROMPT.md`
§10.
## Documentation policy (IMPORTANT)
**Discourse is the canonical source of truth for all knowledge documentation.**
Knowledge docs (architecture, runbooks, references, audits, policies) have been
migrated to [community.turnsys.com](https://community.turnsys.com/c/vp-techops)
as wiki topics in the **VP TechOps** category.
All `.md` files in this repo (except `AGENTS.md` and `LICENSE`) are now
**pointers** that link to their corresponding Discourse topic. **Do not update
documentation content in git** — edit the Discourse wiki topic instead. Git
edit history no longer serves as the documentation changelog; Discourse
preserves wiki edit history automatically.
Code (scripts, configs, playbooks) still lives in git as the source of truth
for executables. Only *documentation* moved to Discourse.
## Top-level files
All `.md` files now point to Discourse. The key pointers:
| File | Points to | Discourse topic |
|------|-----------|-----------------|
| `README.md` | Project overview | [#296](https://community.turnsys.com/t/296) |
| `STATUS.md` | Ticket index + infra summary | [#297](https://community.turnsys.com/t/297) |
| `docs/docmap.md` | Documentation index | [#296](https://community.turnsys.com/t/296) |
Work tracking stays in [Redmine](https://projects.knownelement.com).
**Top-level files:** [`README.md`](README.md) (project overview),
[`STATUS.md`](STATUS.md) (living status, agent-maintained),
[`docs/docmap.md`](docs/docmap.md) (documentation index). Everything else
lives in subdirectories.
## Repository Layout
```
dcinfra/ Data-center infra: PDU (powerman), serial console (console), UPS (ups)
netinfra/ DNS/NTP/DHCP setup + audit; DNS cluster replication (dns-cluster-setup);
switch captures (switches); DHCP config (dhcp)
k8s/ k3s cluster setup scripts (HA control plane over Tailscale) + docs/
proxmox/ Proxmox fleet docs (hardware audit, capacity, storage) + perf tuning (perf/)
awx/ Ansible AWX deployment (k3s + AWX Operator)
tests/ Test suite + VM validation harness + remote.sh SSH chokepoint
scripts/ Framework: git hooks, rule engine (check-rules.sh), shared lib
docs/ Server-build docs, docmap index, and archive
archive/ Historical/superseded code (provisioning -> replaced by KNELIAC project)
vendor/ Vendored KNELShellFramework
provisioning/ Server provisioning (SetupNewSystem.sh, security, 2FA)
tests/ Test suite + VM validation harness
dns-cluster-setup/ Technitium DNS cluster replication
k8s/ k3s cluster setup scripts (3-node HA over Tailscale)
powerman/ Cyclades PM10i PDU management via powerman
console/ Serial console management (ser2net + conman) for switches
perf/ Proxmox perf tuning, fleet audit, iperf
netinfra/ pfv-netinfra-01/02 DNS/NTP setup
switches/ Switch configuration captures
docs/ All documentation (see docs/docmap.md)
vendor/ Vendored KNELShellFramework
```
- **Server provisioning moved to KNELIAC**: The
[`archive/provisioning/`](archive/provisioning/) tree is historical. Active
server provisioning lives in the **KNELIAC** project at
`/home/reachableceo/projects/KNELIAC`.
- **Non-bash files**: Some files under `archive/provisioning/Agents/` have `.sh`
- **Self-locating scripts**: All provisioning scripts derive their own
location via `BASH_SOURCE` and compute `PROJECT_ROOT_PATH` from it. Run
from anywhere.
- **Local configs are the source of truth**: Files in
[`provisioning/ConfigFiles/`](provisioning/ConfigFiles/) are read with
`cat`/`cp`. Do NOT re-introduce `curl ${DL_ROOT}/...` downloads.
- **Non-bash files**: Some files under `provisioning/Agents/` have `.sh`
extension but are PHP (shebang `#!/usr/bin/php`). Skip in syntax checks.
- **Remote access (NON-NEGOTIABLE):** ALL SSH/SCP to ANY host MUST go
through the chokepoint scripts — [`tests/remote.sh`](tests/remote.sh)
(Proxmox hosts + all VMs) or
[`netinfra/dns-cluster-setup/remote-dns.sh`](netinfra/dns-cluster-setup/remote-dns.sh)
(DNS infra hosts: netinfra-01/02, tsrouter, netboot). NEVER call
`ssh`/`scp` directly — the harness blocks raw ssh and the command scanner
rejects it. There are no exceptions.
- **DNS names ONLY (NON-NEGOTIABLE):** NEVER use IP address literals
(neither LAN nor Tailscale IPs) in any command, script, or config.
ALWAYS use DNS names. For Proxmox hosts: `PROX_HOST=<dns-name>`. For VMs:
`VM_IP=<dns-name>`. For conman/SNMP/any tool: pass the DNS name. If a
DNS name does not resolve, fix it in DNS (Technitium) or consult the
[system inventory — Discourse #307](https://community.turnsys.com/t/307).
Do NOT fall back to IP literals. This rule eliminates the per-session
discovery tax of finding the right IP for each host.
- **How to access a production VM:**
1. Look up the DNS name in the [system inventory — Discourse #307](https://community.turnsys.com/t/307).
2. `VM_IP=<dns-name> VM_USER=root bash tests/remote.sh vm '<command>'`
3. If the name does not resolve from the workstation, use `PROX_HOST=<proxmox-node>`
and run `qm guest cmd <vmid> network-get-interfaces` (visibility only —
NOT execution) to find the Tailscale DNS name, then access via that.
- **SSH in Crush**: Direct ssh/scp is blocked. Use
[`tests/remote.sh`](tests/remote.sh) or
[`dns-cluster-setup/remote-dns.sh`](dns-cluster-setup/remote-dns.sh).
## Git Policy
@@ -319,269 +45,53 @@ vendor/ Vendored KNELShellFramework
2. **Atomic commits.** Each commit coherent on its own.
3. **Conventional format**: `feat(scope): desc`, `fix(scope): desc`,
`docs: desc`, `refactor(scope): desc`, `test(scope): desc`.
4. **All shell scripts MUST pass `shellcheck` before commit.** No exceptions.
Run it via the wrapper:
```bash
bash tests/shellcheck.sh # whole repo
bash tests/shellcheck.sh ups/*.sh # specific files
## Automatic Gardening Protocol
**Docs and code must be kept in sync.** After any work session, an agent MUST:
1. **Update [`STATUS.md`](STATUS.md)** — reflect completed work, new issues,
changed infrastructure state. This file is human read-only; agents own it.
2. **Update [`docs/docmap.md`](docs/docmap.md)** — if a doc was added,
removed, or substantively changed, update the table and "Last Reviewed"
date.
3. **Grep for stale paths**`grep -rn 'old/path' --include='*.md'` after
any rename or restructure. Fix all references in the same commit.
4. **Verify new docs are linked** — every new `.md` file must appear in
[`docs/docmap.md`](docs/docmap.md) and be linked from at least one other
doc.
5. **If a new top-level directory was created, update ALL directory listings:**
- [`README.md`](README.md) → "Directory Structure" table
- [`AGENTS.md`](AGENTS.md) → "Repository Layout" code block
- [`AGENTS.md`](AGENTS.md) → "Key Scripts" table (if the directory has
an entrypoint script)
Missing any one of these is a protocol violation.
6. **Self-audit before commit.** Before committing, run:
```
This invokes `koalaman/shellcheck:stable` through Docker (no native binary
needed). Fix every reported finding — including `info`-level — or add a
targeted `# shellcheck disable=SCxxxx # <reason>` directive with a
justification. A script that emits any diagnostic is a protocol violation.
Non-bash scripts (PHP with `.sh` shebang `#!/usr/bin/php`, etc.) are exempt.
## Redmine Tracking Policy
**Redmine is the system of record for all work.** Do not track status,
checklists, or TODOs in repo files. Use Redmine tickets instead.
- **URL:** https://projects.knownelement.com
- **Version:** Potential to Kinetic Ready (due 2026-09-30)
- **Project:** Known Element Enterprises - Technology & Facility Services (id 55)
### Rules
1. **Every piece of work** (feature, fix, deployment, config change) gets a
Redmine ticket. If one doesn't exist, create it.
2. **Reference tickets in docs and commits** using `[#NNN]` notation.
Example: `[#367] Rebuilt k3s control plane after cnode wipe`.
3. **When work completes**, update the ticket: set done ratio to 100%,
add a note describing what was done and where the code lives.
**NEVER close a ticket without explicit user permission.** You may
SUGGEST a close when the result is clearly scoped and delivered. If
it's ambiguous whether the work is truly complete, don't suggest a
close — leave that decision to the user.
4. **Operations Status** lives on Discourse ([topic #297](https://community.turnsys.com/t/297)) — update that wiki topic if the infrastructure summary needs refreshing. The `STATUS.md` file in git is now a pointer only.
5. **Link code to tickets** — ticket descriptions and notes should reference
the relevant file paths in this repo (e.g., `dcinfra/ups/`).
### CLI access (read + write)
Tickets are managed via the `redmine-cli` container, invoked directly
with `docker run` (no wrapper script). Full command reference, patterns, and
the subtask escape hatch live in the CLI's own `AGENTS.md`
(`~/projects/KNEL-AIMiddleware/tooling-cli/redmine/AGENTS.md`) — read it
for anything beyond the basics.
```bash
# Connection sanity check (run first in any session):
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest whoami
# Your queue (project 55):
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55
# Show / create / update / close:
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest show 367
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest create -p 55 -s "Subject" -d "desc"
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest update 367 -n "Done: committed in abc123" --done-ratio 100
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest close 367
```
Key IDs: project **55** (`technicaloperations`), user **5** (`reachableceo`).
Statuses: New(1), In Progress(2), Resolved(3,closed), Feedback(4), Closed(5),
Rejected(6). New subtasks go to **Feedback (4)**. Tracker **3** = Support.
**Gotcha:** `create` has no `--parent` flag — to make a subtask, use the
`python-redmine` escape hatch inside the container (see
`tooling-cli/redmine/AGENTS.md`). Always `show` a ticket before updating it.
Credentials (`REDMINE_URL`/`REDMINE_API_KEY`) live in the centralized store
at `~/.creds/redmine.env`.
## Documentation Workflow
**Discourse is the source of truth for all knowledge docs.** After any work
session, an agent MUST:
1. **Update Discourse wiki topics** — if infrastructure facts changed (new
VM, IP change, host retired), edit the relevant wiki topic at
[community.turnsys.com/c/vp-techops](https://community.turnsys.com/c/vp-techops).
2. **Update the Operations Status topic** ([#297](https://community.turnsys.com/t/297))
if tickets were opened or closed.
3. **Grep for stale paths in code** — `grep -rn 'old/path' --include='*.sh'`
after any rename or restructure. Fix all references in the same commit.
4. **If a new top-level directory was created**, update:
- `AGENTS.md` → "Repository Layout" code block
- `AGENTS.md` → "Key Scripts" table (if it has an entrypoint script)
- Create a new Discourse wiki topic for any documentation
5. **Self-audit before commit.** Code changes must be internally consistent.
Documentation changes go to Discourse, not git.
### CLI access (read + write)
Wiki topics are managed via the `discourse-cli` container, invoked directly
with `docker run` (no wrapper script). Full command reference, patterns, and
the raw-API escape hatch live in the CLI's own `AGENTS.md`
(`~/projects/KNEL-AIMiddleware/tooling-cli/discourse/AGENTS.md`) — read it
for anything beyond the basics.
```bash
# Connection sanity check (run first in any session):
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest whoami
# List VP TechOps topics:
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest ls -c vp-techops
# Show a topic / edit a wiki post (find post id via `show`):
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest show 297
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest update <post_id> -b "new markdown body"
```
VP TechOps = category **74**. Key topics: #296 (project overview), #297
(ops status), #298 (audit log). The API user is trust-level 4 but **not
admin** — admin-only ops (category creation, setting the wiki flag) will
403; surface those to the user rather than retrying. **Gotcha:**
`update`/`delete` take a post **id**, not a post number. Never create a new
topic for an update to existing knowledge — edit the wiki post in place.
Credentials (`DISCOURSE_URL`/`DISCOURSE_API_KEY`/`DISCOURSE_API_USERNAME`)
live in the centralized store at `~/.creds/discourse.env`.
grep -lE 'new_dir_name' README.md AGENTS.md docs/docmap.md STATUS.md
```
Every new top-level directory must appear in all four files.
## Key Scripts
| Script | Purpose |
|--------|---------|
| [`scripts/check-rules.sh`](scripts/check-rules.sh) | Rule audit engine (shellcheck, image pinning, Discourse pointers, required files) |
| [`scripts/setup-hooks.sh`](scripts/setup-hooks.sh) | Install git hooks (pre-commit, pre-push) |
| [`tests/remote.sh`](tests/remote.sh) | **SSH chokepoint** — all Proxmox host + sandbox VM access routes here |
| [`netinfra/dns-cluster-setup/remote-dns.sh`](netinfra/dns-cluster-setup/remote-dns.sh) | SSH chokepoint for DNS infra hosts (netinfra-01/02, tsrouter, netboot) |
| `redmine-cli` container | Redmine CLI (ticket read/write via `docker run`; see `tooling-cli/redmine/`) |
| `discourse-cli` container | Discourse CLI (wiki topic read/write via `docker run`; see `tooling-cli/discourse/`) |
| `dns-cli` container | Technitium DNS CLI (zones, list, add, delete, search, flush; see `tooling-cli/dns/`) |
| [`provisioning/SetupNewSystem.sh`](provisioning/SetupNewSystem.sh) | Full server provisioning |
| [`tests/vm-validation.sh`](tests/vm-validation.sh) | Deploy + validate on sandbox VM |
| [`tests/run-tests.sh`](tests/run-tests.sh) | Test suite |
| [`netinfra/dns-cluster-setup/setup.sh`](netinfra/dns-cluster-setup/setup.sh) | DNS cluster replication |
| [`dns-cluster-setup/setup.sh`](dns-cluster-setup/setup.sh) | DNS cluster replication |
| [`k8s/install-cp.sh`](k8s/install-cp.sh) | Bootstrap k3s HA control plane |
| [`dcinfra/powerman/setup.sh`](dcinfra/powerman/setup.sh) | Configure Cyclades PDU via powerman |
| [`dcinfra/console/setup.sh`](dcinfra/console/setup.sh) | Configure serial console access via ser2net + conman |
| [`dcinfra/ups/setup.sh`](dcinfra/ups/setup.sh) | Configure NUT (Network UPS Tools) for UPS monitoring |
| [`proxmox/perf/deploy-tuning.sh`](proxmox/perf/deploy-tuning.sh) | Deploy perf tunings |
| [`proxmox/perf/scripts/`](proxmox/perf/scripts/) | Read-only audit: probe-storage, probe-network, conman-console, snmp-switch-audit, probe-drift, audit-vm-disks, audit-guest-io, deploy-tuned-guests |
## Switch Console Access (conman)
Switch configs are pulled via serial console through a conman + ser2net
stack on pfv-tsys4.
- **conmand server:** pfv-tsys4 (port 7890)
- **ser2net:** pfv-tsys4, TCP ports 2001-2006
(do NOT connect to ser2net directly — it conflicts with conman's
persistent sessions; always use the conman client)
- **Script:** [`proxmox/perf/scripts/conman-console.py`](proxmox/perf/scripts/conman-console.py)
— drives console sessions read-only via PTY. No expect/tcl required.
- **Command files:** [`netinfra/switches/`](netinfra/switches/) — `.cmds` files
with switch-specific show commands
- **Query available consoles:** `conman -d 100.70.77.93 -q`
```bash
# Pull a switch config (example):
CONMAN_SERVER=100.70.77.93:7890 python3 proxmox/perf/scripts/conman-console.py \
--console pfv-r5-core-01 --cmds netinfra/switches/pfv-r5-core-01.cmds
```
| Console name | TCP port | Device |
|--------------|----------|--------|
| pfv-r5-core-01 | 2001 | Dell PowerConnect 5448 (rack 5 core, mgmt+storage) |
| pfv-r3-tor-mgmt-01 | 2002 | Dell PowerConnect 5324 (rack 3 mgmt TOR) |
| pfv-r3-tor-stor-01 | 2003 | Dell PowerConnect 5324 (rack 3 storage TOR) |
| pfv-rrinfra-rtr | 2004 | Cisco router (rrinfra) |
| pfv-r2-tor-01 | 2005 | Rack 2 TOR switch |
| pfv-r6-mgmt-01 | 2006 | Rack 6 management switch |
## Mandatory: Document ALL Infrastructure Changes
**This is non-negotiable. Every infrastructure change (VM config, disk
cache, network setting, service config, storage migration) MUST be
documented BEFORE moving to the next task step — not "later" or "at the
end."**
Required for EVERY infrastructure change:
1. **Redmine ticket** — create one if none exists. Reference as `[#NNN]`.
2. **Discourse audit log** — reply to topic
[#298](https://community.turnsys.com/t/298) with a dated entry (what
changed, why, where).
3. **Discourse relevant wiki topic** — update the architecture/reference
topic if the change affects documented infrastructure facts (storage
#300, network #299, k8s #305, etc.).
4. **Git commit** — if code/config changed in the repo, commit + push
immediately per the Git Policy above.
## Tooling
- `gh`, `docker`, `jq` available on the workstation.
- No native shellcheck — use `bash tests/shellcheck.sh` (Docker wrapper
`koalaman/shellcheck:stable`). ALL scripts must pass including info-level.
- For raw API calls not covered by the Redmine/Discourse CLIs, use
`python3` inside the CLI Docker containers (escape hatch pattern in
`tooling-cli/{discourse,redmine}/AGENTS.md`).
- `curl`/`wget`/`httpie` may be blocked by some harnesses. Use the CLIs or
the python-in-Docker escape hatch for HTTP writes.
## Key Commands Quick Reference
```bash
# Tests + validation:
bash tests/run-tests.sh # test suite
bash tests/vm-validation.sh # VM validation
bash tests/shellcheck.sh # shellcheck whole repo
bash tests/shellcheck.sh path/to/*.sh # shellcheck specific files
# Redmine + Discourse sanity checks (run first in any session):
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest whoami
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest whoami
# Proxmox host access (DNS names only — never IPs):
PROX_HOST=pfv-tsys5 bash tests/remote.sh prox 'qm list'
PROX_HOST=pfv-tsys5 bash tests/remote.sh prox 'pvesm status'
# Production VM access (DNS names only — never IPs):
VM_IP=tsys-librenms VM_USER=root bash tests/remote.sh vm 'systemctl status cron'
# DNS infra access:
bash netinfra/dns-cluster-setup/remote-dns.sh netinfra01-root 'systemctl status docker'
# Switch console (read-only config pull — DNS names only):
CONMAN_SERVER=pfv-tsys4:7890 python3 proxmox/perf/scripts/conman-console.py \
--console pfv-r5-core-01 --cmds netinfra/switches/pfv-r5-core-01.cmds
```
## Credential Management
- API keys currently in `.env` files under KNEL-AIMiddleware (gitignored).
- User goal: migrate all keys to Hashicorp Vault
(`vault.knownelement.com`). No vault token present yet
(`~/.vault-token` missing). Track as high-priority TODO.
| [`powerman/setup.sh`](powerman/setup.sh) | Configure Cyclades PDU via powerman |
| [`console/setup.sh`](console/setup.sh) | Configure serial console access via ser2net + conman |
| [`perf/deploy-tuning.sh`](perf/deploy-tuning.sh) | Deploy perf tunings |
## Key Docs
→ **All documentation lives on Discourse:**
[community.turnsys.com/c/vp-techops](https://community.turnsys.com/c/vp-techops)
→ All `.md` files in this repo are pointers to Discourse topics.
**Complete Linux System Inventory:**
[Topic #307](https://community.turnsys.com/t/307) — every Linux system
(hosts + VMs + physical), with Tailscale IPs, DNS names, SSH access
status, and tuned profiles. Reference this for monitoring coverage,
access management, and hostname consistency.
→ **See [`docs/docmap.md`](docs/docmap.md) for the full documentation index.**
## Project Context
Solo-founder R&D Proxmox cluster in a private residence. Shoestring budget.
Production lives on a Cloudron VPS in Reston VA. See the
[Operations Status topic](https://community.turnsys.com/t/297) for the ticket
index and infrastructure summary.
All work is tracked in [Redmine](https://projects.knownelement.com)
(version: Potential to Kinetic Ready, due 2026-09-30).
Production lives on a Cloudron VPS in Reston VA. See
[`STATUS.md`](STATUS.md) for current state and
[`docs/proxmox/PROJECT.md`](docs/proxmox/PROJECT.md) for the fleet report.
-50
View File
@@ -1,50 +0,0 @@
# Makefile — convenience dispatch to scripts/.
#
# Not required. The scripts in scripts/ are the real entry points and work
# standalone. This file just gives you short verbs if you're at a terminal.
#
# In Mode 2 (Hermes/OWUI/MCP), agents call the scripts directly or via API —
# they don't need this file.
# Project-specific overrides for check-rules.sh
export PROJECT_DOC_EXEMPT ?= AGENTS.md STATUS.md WORKING.md README.md ADOPTING.md LICENSE .env.example questions-v1.md BASELINE-PROMPT.md PATTERNS.md
export PROJECT_DISCOURSE_HOST ?= community.turnsys.com
.PHONY: setup validate fast lint test garden up down status clean help
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
setup: ## Install git hooks
@bash scripts/setup-hooks.sh
validate: ## Full rule audit (includes tests)
@bash scripts/check-rules.sh
fast: ## Fast rule audit (pre-commit equivalent)
@bash scripts/check-rules.sh --fast
lint: ## Lint shell scripts (shellcheck via docker)
@docker run --rm -v "$$(pwd):/mnt" koalaman/shellcheck:stable \
$$(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print | sed 's|^\./|/mnt/|') || true
test: ## Run the test suite (override per project)
@bash scripts/test.sh
garden: ## Doc-sprawl / Discourse-migration report
@bash scripts/garden.sh
up: ## Bring up the docker-compose stack
@bash scripts/up.sh
down: ## Bring down the docker-compose stack
@bash scripts/down.sh
status: ## Show repo status snapshot
@echo "== branch =="; git branch --show-current 2>/dev/null || echo "(no branch)"
@echo "== last commit =="; git log --oneline -1 2>/dev/null || true
@echo "== working tree =="; git status --short 2>/dev/null || echo "(not a git repo)"
@echo "== STATUS.md head =="; sed -n '1,12p' STATUS.md 2>/dev/null || echo "(no STATUS.md)"
clean: ## Remove build/test artifacts (override per project)
@echo "make clean: nothing to clean — override this in your project's Makefile."
+50 -9
View File
@@ -1,10 +1,51 @@
# README.md
# PFVCluster
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Project overview, architecture, quick start**
>
> **Read it here:** https://community.turnsys.com/t/296
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
Unified infrastructure repo for the Known Element Enterprises Proxmox R&D cluster.
**[→ Current Status](STATUS.md)** · **[→ Documentation Index](docs/docmap.md)** · **[→ Agent Guidelines](AGENTS.md)**
## Directory Structure
| Directory | Description |
|-----------|-------------|
| [`provisioning/`](provisioning/) | Server provisioning (SetupNewSystem.sh, security hardening, 2FA, NTP/DNS config, SNMP, Dell OMSA) |
| [`tests/`](tests/) | Test suite + VM validation harness |
| [`dns-cluster-setup/`](dns-cluster-setup/) | Technitium DNS cluster replication scripts |
| [`k8s/`](k8s/) | k3s cluster setup scripts (3-node HA control plane over Tailscale) |
| [`powerman/`](powerman/) | Cyclades PM10i PDU management via powerman on pfv-tsys1 |
| [`console/`](console/) | Serial console management (ser2net + conman) for network switches on pfv-tsys4 |
| [`perf/`](perf/) | Proxmox performance tuning, fleet audit, iperf, switch diagnostics |
| [`netinfra/`](netinfra/) | pfv-netinfra-01/02 DNS/NTP setup + audit scripts |
| [`switches/`](switches/) | Switch configuration captures |
| [`docs/`](docs/) | All documentation ([see docmap](docs/docmap.md)) |
| [`vendor/`](vendor/) | Vendored KNELShellFramework |
## Quick Start
### Provision a new server
```bash
sudo bash provisioning/SetupNewSystem.sh
```
### Validate on the sandbox VM
```bash
VM_ID=6000 ./tests/vm-validation.sh all
```
### Deploy DNS cluster
```bash
cd dns-cluster-setup/ && ./setup.sh all
```
### Deploy perf tunings
```bash
cd perf/ && ./deploy-tuning.sh
```
## Architecture
- **Proxmox hosts**: 7 standalone PVE installs managed via PDM
- **DNS**: Technitium (authoritative) + Pi-hole (recursive) on pfv-netinfra-01/02
- **NTP**: pfv-netinfra-01/02 (redundant, LAN IPs, stratum 2/3)
- **Production**: Cloudron VPS in Reston VA (this cluster is R&D only)
- **Backups**: Proxmox Backup Server (PBS)
+117 -9
View File
@@ -1,10 +1,118 @@
# STATUS.md
# Project Status
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Ticket index + infrastructure summary**
>
> **Read it here:** https://community.turnsys.com/t/297
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
> **Human read-only. Agents maintain this file automatically after each work
> session.** Do not edit by hand — the next agent run will overwrite it.
> **Last updated:** 2026-07-28 by Crush (GLM-5.2)
## Current State: STABLE
The merged PFVCluster repo is fully operational across provisioning, DNS
infrastructure, Proxmox cluster ops, and k8s control plane.
## Completed Work
### Server Provisioning (validated on sectestbed-sandbox)
- [x] SetupNewSystem.sh deploys end-to-end to rc=0 (Debian 13 trixie)
- [x] Security hardening: SSH, SCAP-STIG, Wazuh, 2FA (SSH+Cockpit+Webmin)
- [x] NTP: redundant pfv-netinfra-01/02 (192.168.3.252/253), synced stratum 3
- [x] DNS resolv.conf: managed static file pointing at netinfra pair
- [x] Test suite: 5 tests (framework, safe-download, 2fa, https, system-req)
- [x] VM validation harness: git-based deploy + auto-rollback + guest-agent access
### DNS Cluster (pfv-netinfra-01/02)
- [x] Production Technitium config replicated from tailscale-router (read-only)
- [x] 124 zones on both nodes (knel.net + reverse DNS)
- [x] pfv-netinfra-01 = PRIMARY, pfv-netinfra-02 = SECONDARY
- [x] Zone replication via rsync systemd timer (every 60s)
- [x] Credentials + 2FA replicated identically to production
- [x] Both LAN IPs resolve knel.net device names + recurse externally
### Proxmox Cluster Ops
- [x] 5 of 7 hosts fully performance-tuned (tsys1/3/6/7/9)
- [x] Fleet audit complete (PROJECT.md has ground truth)
- [ ] tsys4: blocked on PCIe NIC + RAM install
- [ ] tsys5: blocked on 2nd ethernet cable + NVMe install
- [ ] tsys2: pending rebuild from Win10 to Proxmox
### Kubernetes Control Plane (k3s HA — LIVE)
- [x] 3-node k3s HA control plane deployed: cnode1/2/3 (v1.36.2+k3s1, embedded etcd)
- [x] **All traffic over Tailscale IPs** — no LAN IPs in node status or certs
- [x] All 3 cnodes tainted `control-plane:NoSchedule` (zero user workloads)
- [x] 13/13 health checks pass (verify.sh): nodes Ready, etcd quorum,
Tailscale IPs, CoreDNS, API server, workload isolation
- [x] Scripts in `k8s/`: wipe, install-cp, join-servers, post-setup, verify
- [x] Kubeconfig saved to `~/.kube/config.pfv-k8s` (gitignored, embedded certs)
- [ ] Workers (wnodes) not yet joined to this cluster
- [ ] Distro decision: **k3s chosen for regular R&D cluster**. Talos docs
preserved in `docs/k8s/` for future ITAR/classified cluster.
### PDU Management (powerman on pfv-tsys1 — LIVE)
- [x] Cyclades AlterPath PM10i (10 outlets) managed via powerman over serial
- [x] USB-DB9 adapter (Prolific pl2303) with stable udev symlink
`/dev/cyclades-pm10`
- [x] powermand listening on `127.0.0.1:10101` + Tailscale `100.121.189.98:10101`
- [x] All 10 outlets defined as `outlet-1` through `outlet-10`
- [x] Validated: outlet 10 cycled off → on (8/8 test checks passed)
- [ ] Rename outlets to match physical devices (Friday onsite)
### Console Management (ser2net + conman on pfv-tsys4 — LIVE)
- [x] 7 network switch/router consoles managed via ser2net + conman
- [x] **USB enumeration problem SOLVED:** udev rules pin each adapter by
ID_PATH (physical USB port topology) to stable `/dev/consoles/<name>`
symlinks that survive reboot regardless of enumeration order
- [x] ser2net exposes all 7 consoles on TCP ports (2001-2007) bound to
**Tailscale IP only** (`100.70.77.93`)
- [x] conman connects to TCP ports for logging + multiplexing (7 log
files active in `/var/log/conman/`)
- [x] Both ser2net + conmand enabled via systemd (survive reboot)
- [x] conmand systemd unit created (Debian package doesn't ship one)
- [x] Old `/root/conmap` + manual `screen` workflow replaced
### Repo Merge
- [x] KNELServerBuild merged into PFVCluster (history preserved)
- [x] Directory structure reorganized (provisioning/, tests/, perf/, docs/)
- [x] All docs gardened: links fixed, stale refs removed, tailscale.md updated
## Known Issues
| Issue | Impact | Status |
|-------|--------|--------|
| **UCS + netinfra HA pairs both on tsys4** | tsys4 failure = DNS/DHCP/NTP + LDAP/AD fully dark | **CRITICAL — needs PDM migration** |
| **2 of 3 k3s cnodes on tsys4** | tsys4 failure = etcd quorum lost | **CRITICAL — needs PDM migration** |
| Technitium AXFR uses port 53 (occupied by Pi-hole) | Zone transfer via rsync instead of native AXFR | Workaround in place |
| `download.proxmox.com` unreachable from sandbox VM | 2 validation tests warn (environmental) | Not a code issue |
| tsys4/5 hardware pending | Perf tuning incomplete on 2 hosts | Waiting on physical install |
## Pending (next session priorities)
1. **CRITICAL: Migrate HA pairs to separate storage** (PDM, 10 min):
- netinfra-02 (VMID 904): D2 → S3 (tsys4 → tsys5)
- ucs-02 (VMID 902): D5 → S2 (tsys4 → tsys5)
2. **CRITICAL: Fix k3s cnode quorum risk** (PDM, 5 min):
- Move cnode1 (VMID 906) or cnode2 (VMID 705) from tsys4 to tsys5 storage
3. **k8s workers:** Join wnodes to the k3s cluster (agents, not servers)
4. **PDU:** Rename outlets in powerman.conf to match physical devices (Friday)
5. Perf: complete tsys4/5 tuning after hardware install (Friday)
6. tsys2: rebuild from Win10 to Proxmox (k8s-dedicated host)
7. **k8s deferred topics:** ETL tooling, HPC scheduler, vcluster policy,
solar-aware scale-out
> **See [`docs/proxmox/AUDIT-2026-07-28.md`](docs/proxmox/AUDIT-2026-07-28.md)
> for the full fresh audit with VM inventory and action items.**
## Infrastructure Summary
| Component | Details |
|-----------|---------|
| Proxmox hosts | 7 standalone PVE, managed via PDM |
| DNS primary | pfv-netinfra-01 (192.168.3.252) — Technitium + Pi-hole |
| DNS secondary | pfv-netinfra-02 (192.168.3.253) — Technitium + Pi-hole |
| DNS production | tailscale-router (read-only source of truth) |
| NTP | pfv-netinfra-01/02 (redundant, LAN IPs, stratum 2/3) |
| Sandbox VM | sectestbed-sandbox (VMID 6000 on pfv-tsys5) |
| Backup | Proxmox Backup Server (PBS) |
| **k8s control plane** | **3-node k3s HA (cnode1/2/3), all traffic over Tailscale** |
| **PDU** | **Cyclades PM10i via powerman on pfv-tsys1 (port 10101)** |
| **Console** | **7 switch consoles via ser2net+conman on pfv-tsys4 (TCP 2001-2007 on Tailscale)** |
| Production | Cloudron VPS, Reston VA (this cluster is R&D only) |
-11
View File
@@ -1,11 +0,0 @@
# WORKING.md — Active Session Tracker
Agent work only. The human decides when it's done.
A commit is blocked while any task below remains unchecked.
## Current Tasks
(all done — session complete)
- [x] Add ticket-gate Crush hook (blocks work without active ticket)
- [x] Document active-ticket workflow in AGENTS.md + meta template
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/bash
# access-matrix.sh — definitive access verification across all Linux Tailscale nodes.
# Uses the correct SSH user(s) per system type, checks sudo where applicable.
# Routes through remote.sh (the only allowed ssh path).
set -u
cd /home/reachableceo/projects/PFVCluster || exit 1
# Policy-excluded systems (never attempt access)
EXCLUDE=':tsys-cloudron:pfv-bms:tsys-umbrel:tsys-ucs-01:tsys-ucs-02:stlpc-bizoffice:ultix-highside:'
# Determine the SSH user(s) for a given hostname and whether sudo is expected.
# Returns "user1:user2:...:sudoflag" where sudoflag is "yes" or "no".
users_for() {
local name="$1"
case "$name" in
pfv-tsys[0-9]) echo "root:no" ;;
*-proxmox-datacenter) echo "root:no" ;;
*-proxmox-pve) echo "root:no" ;;
*-proxmox-pbs) echo "root:no" ;;
*-proxmox-mailgw*) echo "root:no" ;;
*-proxmox-backup*) echo "root:no" ;;
tsys-ucs-*) echo "root:no" ;;
ultix-streaming) echo "root:no" ;;
stlpc-*) echo "root:labuser:no" ;;
ultix-field) echo "ultixfield:yes" ;;
subopi*) echo "subodev:yes" ;;
*) echo "localuser:yes" ;;
esac
}
check_user() {
local ip="$1" user="$2" expect_sudo="$3"
local out sudo
out=$(VM_IP="$ip" VM_USER="$user" bash tests/remote.sh vm 'echo SSHOK; id -un' </dev/null 2>&1 | grep -oE 'SSHOK|keyboard-interactive|Connection refused' | head -1)
case "$out" in
SSHOK)
if [ "$expect_sudo" = "yes" ]; then
sudo=$(VM_IP="$ip" VM_USER="$user" bash tests/remote.sh vm 'sudo -n true 2>/dev/null && echo SUDOOK || echo SUDONO' </dev/null 2>&1 | grep -oE 'SUDOOK|SUDONO' | head -1)
printf '%s(%s)' "$user" "${sudo:-?}"
else
printf '%s(ok)' "$user"
fi
;;
keyboard-interactive) printf '%s(2FA)' "$user" ;;
'Connection refused') printf '%s(NOSSH)' "$user" ;;
*) printf '%s(NOKEY)' "$user" ;;
esac
}
printf '%-32s %-16s %s\n' "NAME" "TS-IP" "ACCESS"
printf '%-32s %-16s %s\n' "----" "-----" "------"
tailscale status 2>/dev/null | awk '$4=="linux" {print $2, $1}' | sort | while read -r name ip; do
[ -n "$name" ] || continue
case "$EXCLUDE" in *":$name:"*) printf '%-32s %-16s %s\n' "$name" "$ip" "EXCLUDED"; continue;; esac
map=$(users_for "$name")
expect_sudo="${map##*:}"
users="${map%:*}"
result=""
IFS=':' read -ra user_list <<< "$users"
for u in "${user_list[@]}"; do
r=$(check_user "$ip" "$u" "$expect_sudo")
[ -z "$result" ] && result="$r" || result="$result $r"
done
printf '%-32s %-16s %s\n' "$name" "$ip" "$result"
done
-60
View File
@@ -1,60 +0,0 @@
#!/bin/sh
# agent-bootstrap.sh
#
# Run INSIDE a guest (via SSH, console, or guest-agent) to bring the
# system fully under agent management in one shot:
# 1. install + enable qemu-guest-agent (VMs only, skipped on bare metal)
# 2. push the agent SSH key to root + AGENT_USER (+ labuser if present)
# 3. grant AGENT_USER passwordless sudo
#
# AGENT_USER defaults to "localuser". Override for systems with a different
# unprivileged agent user:
# AGENT_USER=subodev bash agent-bootstrap.sh
#
# After this runs once, the agent has SSH+sudo immediately.
#
# Usage (from a root shell in the guest):
# bash agent-bootstrap.sh
# AGENT_USER=subodev bash agent-bootstrap.sh
set -eu
KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIWms/uCXnjjo4KyxHBcYI2TDHe8OZ2wle6W/0hSRQLu reachableceo@ultix-streaming'
AGENT_USER="${AGENT_USER:-localuser}"
# 1. guest-agent (skip on bare metal — no virtio-serial device)
if command -v systemd-detect-virt >/dev/null 2>&1 && \
[ "$(systemd-detect-virt --vm 2>/dev/null || echo none)" != "none" ]; then
if ! command -v qemu-ga >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y qemu-guest-agent
elif command -v dnf >/dev/null 2>&1; then
dnf install -y qemu-guest-agent
elif command -v yum >/dev/null 2>&1; then
yum install -y qemu-guest-agent
else
echo "WARN: no supported package manager; skipping agent install" >&2
fi
fi
systemctl enable --now qemu-guest-agent 2>/dev/null || \
systemctl enable --now qemu-ga 2>/dev/null || true
fi
# 2. SSH key for root + AGENT_USER + labuser (if present)
for u in root "$AGENT_USER" labuser; do
if ! getent passwd "$u" >/dev/null 2>&1; then continue; fi
H=$(getent passwd "$u" | cut -d: -f6)
mkdir -p "$H/.ssh"; chmod 700 "$H/.ssh"
AK="$H/.ssh/authorized_keys"; touch "$AK"; chmod 600 "$AK"
grep -qF "$KEY" "$AK" || echo "$KEY" >> "$AK"
chown -R "$u": "$H/.ssh"
done
# 3. passwordless sudo for AGENT_USER only
if getent passwd "$AGENT_USER" >/dev/null 2>&1 && [ -d /etc/sudoers.d ]; then
echo "${AGENT_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/010-agent
chmod 440 /etc/sudoers.d/010-agent
fi
echo BOOTSTRAP-DONE
@@ -1,23 +0,0 @@
# PFV NFS tuning sysctl overrides
#
# Applied AFTER tuned via pfv-nfs-tuning.service (systemd oneshot).
# These override tuned's network-throughput/virtual-host 16MB TCP buffer
# caps with 128MB for high-BDP NFS over 1-4 GbE LACP links.
#
# Install on ALL Proxmox hosts:
# cp 99-pfv-nfs.conf /etc/sysctl.d/99-pfv-nfs.conf
# cp pfv-nfs-tuning.service /etc/systemd/system/pfv-nfs-tuning.service
# systemctl daemon-reload && systemctl enable --now pfv-nfs-tuning.service
#
# Created: 2026-07-31
# Deployed: tsys1, tsys3, tsys4, tsys5, tsys6, tsys7, tsys9
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.rmem_default = 26214400
net.core.wmem_default = 26214400
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 65535
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.ipv4.tcp_max_syn_backlog = 4096
@@ -1,28 +0,0 @@
# PFV NFS tuning service
#
# Systemd oneshot that runs AFTER tuned.service to apply TCP buffer
# overrides. The tuned daemon's profiles (network-throughput for storage
# hosts, virtual-host for compute hosts) set 16MB TCP buffer caps which
# are too small for high-BDP NFS over LACP links. This service force-
# applies 128MB buffers after tuned has finished its configuration.
#
# Install:
# cp pfv-nfs-tuning.service /etc/systemd/system/pfv-nfs-tuning.service
# systemctl daemon-reload
# systemctl enable --now pfv-nfs-tuning.service
#
# Created: 2026-07-31
# Deployed: all 7 Proxmox hosts (tsys1/3/4/5/6/7/9)
[Unit]
Description=PFV NFS tuning (override tuned TCP buffer caps)
After=tuned.service
Requires=tuned.service
[Service]
Type=oneshot
ExecStart=/sbin/sysctl -p /etc/sysctl.d/99-pfv-nfs.conf
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
# auth-cloudron-ldap.sh — placeholder module (Cloudron LDAP auth integration).
# Intentionally empty; populated when the auth stack is deployed.
true
@@ -1,2 +0,0 @@
# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment
export HISTTIMEFORMAT="%m/%d/%Y %T "
-9
View File
@@ -1,9 +0,0 @@
<!-- Discourse: https://community.turnsys.com/t/298 -->
<!-- Redmine: https://projects.knownelement.com/issues/314 -->
# Session Handoff: 2026-08-11/12 OAM
**Full handoff lives in Redmine #314** (session summary note, 2026-08-12).
**OAM coverage matrix:** Discourse [#309](https://community.turnsys.com/t/309).
**Architecture rules:** Discourse [#303](https://community.turnsys.com/t/303).
**Open questions:** `questions-v2.md` in this repo.
-10
View File
@@ -1,10 +0,0 @@
# awx/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Ansible AWX deployment on k3s**
>
> **Read it here:** https://community.turnsys.com/t/302
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-46
View File
@@ -1,46 +0,0 @@
---
# AWX namespace
apiVersion: v1
kind: Namespace
metadata:
name: awx
---
# Admin password secret — the password is 'REDACTED_PASSWORD' (fleet standard)
apiVersion: v1
kind: Secret
metadata:
name: awx-admin-password
namespace: awx
type: Opaque
stringData:
password: REDACTED_PASSWORD
---
# AWX Custom Resource — single instance, LoadBalancer service
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: tsys-awx
namespace: awx
spec:
service_type: LoadBalancer
ingress_type: none
admin_user: admin
admin_password_secret: awx-admin-password
# PostgreSQL — bundled, stored on local disk via PVC (k3s local-path)
postgres_storage_class: local-path
postgres_storage_requirements:
requests:
storage: 8Gi
postgres_resource_requirements:
requests:
memory: 1Gi
# Resource limits — fit within 12 GB host RAM
web_resource_requirements:
requests:
memory: 1Gi
task_resource_requirements:
requests:
memory: 1Gi
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env bash
###############################################################################
# deploy-awx.sh — Deploy AWX Operator + instance on k3s.
#
# Prerequisites: k3s must be installed and running (install-k3s.sh).
# Intended to run ON the target VM (tsys-awx.knel.net) as root or via sudo.
#
# Usage: sudo bash deploy-awx.sh
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export KUBECONFIG="${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}"
OPERATOR_VERSION="${OPERATOR_VERSION:-2.19.1}"
echo "=========================================================="
echo " AWX Operator deployment — version ${OPERATOR_VERSION}"
echo "=========================================================="
# ---------------------------------------------------------------------------
# Step 1: Create namespace
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 1: Create namespace ==="
kubectl apply -f "${SCRIPT_DIR}/namespace.yaml"
# ---------------------------------------------------------------------------
# Step 2: Deploy AWX Operator
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 2: Deploy AWX Operator ${OPERATOR_VERSION} ==="
# Clone the operator to get kustomize manifests
OPERATOR_DIR="/tmp/awx-operator-${OPERATOR_VERSION}"
rm -rf "${OPERATOR_DIR}"
git clone --branch "${OPERATOR_VERSION}" --depth 1 \
"https://github.com/ansible/awx-operator.git" "${OPERATOR_DIR}" 2>&1 | tail -3
# The operator's default namespace is 'awx' — matches our setup
# Apply the operator via kustomize (config/default has the full manifest set)
kubectl apply -k "${OPERATOR_DIR}/config/default" 2>&1 || {
echo "kustomize apply failed, trying raw manifests..."
kubectl apply -f "https://raw.githubusercontent.com/ansible/awx-operator/${OPERATOR_VERSION}/deploy/awx-operator.yaml"
}
# Fix kube-rbac-proxy image (gcr.io/kubebuilder/kube-rbac-proxy was removed;
# quay.io/brancz/kube-rbac-proxy is the maintained replacement)
echo ""
echo "=== Patching kube-rbac-proxy image ==="
kubectl set image deployment/awx-operator-controller-manager -n awx \
kube-rbac-proxy=quay.io/brancz/kube-rbac-proxy:v0.15.0 2>&1 || true
# Scale down any old replicasets that still reference the broken image
for rs in $(kubectl -n awx get rs -l control-plane=controller-manager -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null); do
img=$(kubectl -n awx get rs "${rs}" -o jsonpath='{.spec.template.spec.containers[?(@.name=="kube-rbac-proxy")].image}' 2>/dev/null)
if [[ "${img}" == *"gcr.io/kubebuilder"* ]]; then
echo "Scaling down old RS ${rs} (has broken gcr.io image)"
kubectl -n awx scale rs "${rs}" --replicas=0 2>&1
fi
done
echo ""
echo "Waiting for AWX Operator deployment to be ready..."
kubectl -n awx wait --for=condition=Available deployment/awx-operator-controller-manager \
--timeout=300s 2>&1 || {
echo "Operator not ready yet — checking status..."
kubectl -n awx get pods
}
# ---------------------------------------------------------------------------
# Step 3: Deploy AWX instance
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 3: Deploy AWX instance ==="
kubectl apply -f "${SCRIPT_DIR}/awx-instance.yaml"
echo ""
echo "AWX instance created. Operator will now reconcile."
echo "This typically takes 5-10 minutes for the first deployment."
echo ""
echo "Monitor progress with:"
echo " kubectl -n awx get awx tsys-awx -o jsonpath='{.status.conditions}' | jq ."
echo " kubectl -n awx get pods -w"
echo " kubectl -n awx logs deployment/awx-operator-controller-manager -f"
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
###############################################################################
# install-k3s.sh — Install k3s single-node on the tsys-awx VM.
#
# Intended to run ON the target VM (tsys-awx.knel.net) as root or via sudo.
# Installs k3s without Traefik (we use NodePort/LoadBalancer directly).
#
# Usage: sudo bash install-k3s.sh
###############################################################################
set -euo pipefail
NODE_NAME="${NODE_NAME:-tsys-awx}"
echo "=========================================================="
echo " k3s single-node install — ${NODE_NAME}"
echo "=========================================================="
if command -v k3s >/dev/null 2>&1 && k3s kubectl get nodes >/dev/null 2>&1; then
echo "k3s already installed and running. Skipping."
k3s kubectl get nodes
exit 0
fi
echo ""
echo "=== Installing k3s (this takes 1-2 minutes) ==="
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --write-kubeconfig-mode=644" sh -
echo ""
echo "=== Waiting for k3s node to be Ready ==="
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
for i in $(seq 1 30); do
if k3s kubectl get nodes 2>/dev/null | grep -q ' Ready'; then
echo "Node is Ready!"
k3s kubectl get nodes
break
fi
echo " waiting... (${i}/30)"
sleep 5
done
echo ""
echo "=== k3s install complete ==="
echo "kubeconfig: /etc/rancher/k3s/k3s.yaml"
echo "kubectl: k3s kubectl (or set KUBECONFIG=/etc/rancher/k3s/k3s.yaml)"
-6
View File
@@ -1,6 +0,0 @@
---
# AWX Operator namespace
apiVersion: v1
kind: Namespace
metadata:
name: awx
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env bash
###############################################################################
# verify-awx.sh — Verify AWX deployment status and access.
#
# Intended to run ON the target VM (tsys-awx.knel.net).
# Usage: bash verify-awx.sh
###############################################################################
set -euo pipefail
export KUBECONFIG="${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}"
echo "=========================================================="
echo " AWX Deployment Verification — $(date)"
echo "=========================================================="
echo ""
echo "=== 1. k3s node ==="
kubectl get nodes
echo ""
echo "=== 2. AWX pods ==="
kubectl -n awx get pods
echo ""
echo "=== 3. AWX CR status ==="
kubectl -n awx get awx tsys-awx -o jsonpath='{range .status.conditions[*]}{.type}: {.message}{"\n"}{end}' 2>/dev/null || echo "AWX CR not found"
echo ""
echo "=== 4. Services ==="
kubectl -n awx get svc
echo ""
echo "=== 5. LoadBalancer / NodePort access ==="
LB_IP=$(kubectl -n awx get svc tsys-awx-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
LB_HOST=$(kubectl -n awx get svc tsys-awx-service -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "")
NODE_PORT=$(kubectl -n awx get svc tsys-awx-service -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "")
if [ -n "${LB_IP}" ]; then
echo "LoadBalancer IP: ${LB_IP}"
ACCESS_URL="http://${LB_IP}"
elif [ -n "${LB_HOST}" ]; then
echo "LoadBalancer hostname: ${LB_HOST}"
ACCESS_URL="http://${LB_HOST}"
elif [ -n "${NODE_PORT}" ]; then
echo "NodePort: ${NODE_PORT}"
ACCESS_URL="http://$(hostname -I | awk '{print $1}'):${NODE_PORT}"
else
echo "Service not ready yet"
ACCESS_URL=""
fi
echo ""
echo "=== 6. Admin password ==="
ADMIN_PASS=$(kubectl -n awx get secret awx-admin-password -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || echo "")
if [ -n "${ADMIN_PASS}" ]; then
echo "User: admin"
echo "Password: ${ADMIN_PASS}"
else
echo "Admin password secret not found"
fi
echo ""
echo "=== 7. HTTP check ==="
if [ -n "${ACCESS_URL}" ]; then
echo "Testing ${ACCESS_URL}..."
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "${ACCESS_URL}" 2>/dev/null || echo "failed")
echo "HTTP response: ${HTTP_CODE}"
if [ "${HTTP_CODE}" = "200" ] || [ "${HTTP_CODE}" = "302" ] || [ "${HTTP_CODE}" = "301" ]; then
echo "✓ AWX is accessible at ${ACCESS_URL}"
else
echo "✗ AWX not yet responding (HTTP ${HTTP_CODE})"
fi
fi
echo ""
echo "=========================================================="
if [ -n "${ACCESS_URL}" ]; then
echo " AWX Access URL: ${ACCESS_URL}"
fi
echo "=========================================================="
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/bash
# bootstrap-all.sh — push agent SSH key + passwordless sudo to remaining systems.
#
# SSH is the ONLY approved access channel (see AGENTS.md "Access-channel
# policy: SSH only"). This script reaches systems that still allow password
# auth over sshd. Systems that reject password auth (publickey-only) cannot
# be reached this way — see the CONSOLE-ONLY section printed at the end.
#
# Two escalation methods:
# sudo → Ubuntu-style systems (no root pw; localuser has sudo)
# su → Debian-style systems (root has a password)
#
# Passes AGENT_USER so agent-bootstrap.sh targets the correct unprivileged
# user. You enter passwords interactively. Idempotent: safe to re-run.
#
# Verified state (access-matrix.sh, 2026-08-10): 68/70 non-excluded systems
# at intended access state. Only tsys-siem remains below.
set -u
cd "$(dirname "$0")" || exit 1
SCRIPT=agent-bootstrap.sh
SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10)
run_with_sudo() {
local name="$1" ip="$2" user="$3" agent_user="${4:-localuser}"
echo "========================================"
echo " $name ($ip) — $user (sudo, agent=${agent_user})"
echo "========================================"
scp "${SSH_OPTS[@]}" "$SCRIPT" "${user}@${ip}:/tmp/" \
&& ssh -t "${SSH_OPTS[@]}" "${user}@${ip}" "sudo AGENT_USER=${agent_user} bash /tmp/$SCRIPT" \
&& echo " -> $name DONE" \
|| echo " -> $name FAILED"
echo
}
run_with_su() {
local name="$1" ip="$2" user="$3" agent_user="${4:-localuser}"
echo "========================================"
echo " $name ($ip) — $user (su, agent=${agent_user})"
echo "========================================"
scp "${SSH_OPTS[@]}" "$SCRIPT" "${user}@${ip}:/tmp/" \
&& ssh -t "${SSH_OPTS[@]}" "${user}@${ip}" "su -c 'AGENT_USER=${agent_user} bash /tmp/$SCRIPT'" \
&& echo " -> $name DONE" \
|| echo " -> $name FAILED"
echo
}
# All password-auth-reachable systems have been bootstrapped.
# Verified state (access-matrix.sh, 2026-08-10): 69/70 non-excluded systems
# at intended access state. The only remaining NOKEY (stlp-3dscanner) is
# deferred to [#417] and requires a rename + bring-online first — out of
# scope here. Run `access-matrix.sh` to re-verify at any time.
echo "All password-auth-reachable systems are bootstrapped."
echo "Remaining gap: stlp-3dscanner (deferred to [#417])."
echo "Run access-matrix.sh to re-verify."
echo "Deferred (separate ticket):"
echo " stlp-3dscanner — rename + bring online first [#417]"
echo "========================================"
echo "By design (leave alone):"
echo " sectestbed-sandbox — 2FA enforced"
echo "========================================"
echo "Excluded by policy (no SSH access):"
echo " pfv-bms (API), tsys-cloudron (prod revenue),"
echo " tsys-ucs-01/02 (API-managed), tsys-umbrel (treasury)"
echo "========================================"
+121
View File
@@ -0,0 +1,121 @@
# Console Management (ser2net + conman)
Network-accessible serial console management for all production network
switches and routers, running on **pfv-tsys4** (storage server).
## Architecture
```
USB-DB9 adapters → udev symlinks (/dev/consoles/<name>) → ser2net (TCP) → conman (logging + multiplexing)
```
ser2net owns the physical serial devices and exposes them on TCP ports
bound to the **Tailscale interface only** (`100.70.77.93:200X`). conman
connects to those TCP ports for session logging, output capture, and
multi-user console sharing.
**conman and ser2net do NOT share ports** — only one process can open a
serial device at a time. ser2net owns the physical device; conman connects
over TCP.
## The USB Enumeration Problem (SOLVED)
The 9 Prolific USB-to-DB9 adapters (`067b:2303`) on pfv-tsys4 have **no
unique USB serial numbers** and get assigned `/dev/ttyUSB0-8` based on
enumeration order, which shifts on every boot. This made the old
`/root/conmap` + manual `screen` workflow break after every reboot.
**Fix:** udev rules pin each adapter by its **ID_PATH** (physical USB port
topology), which is stable across reboots regardless of enumeration order.
Each adapter gets a named symlink in `/dev/consoles/` that never changes.
The udev rules are generated from `mapping.txt`, which maps each adapter's
ID_PATH to a console name and TCP port. To re-map after physically moving
an adapter, update `mapping.txt` and re-run `setup.sh`.
**Fallback:** if udev trigger doesn't create symlinks for already-discovered
devices (common on first run), `setup.sh` creates them manually by matching
ID_PATH. On subsequent boots, udev creates them automatically.
## Port Assignments
| TCP Port | Console Name | ID_PATH | Description |
|----------|-------------|---------|-------------|
| 2001 | pfv-core-sw01 | usb-0:1.5.4.4 | Dell PowerConnect 5448 (core switch) |
| 2002 | pfv-tor3-mgmt | usb-0:1.6.3.1 | Rack 3 management TOR switch |
| 2003 | pfv-tor3-stor | usb-0:1.6.3.3.2 | Rack 3 storage TOR switch |
| 2004 | pfv-rrinfra-rtr | usb-0:1.6.3.3.1 | Cisco router (rrinfra) |
| 2005 | pfv-r2-tor-top | usb-0:1.6.3.3.3 | Rack 2 top-of-rack switch |
| 2006 | subodev-torsw | usb-0:1.5.4.1 | Suborbital device TOR switch |
| 2007 | pfv-r2-sw | usb-0:1.6.3.2 | Rack 2 old Dell switch |
All ports listen on the Tailscale IP (`100.70.77.93`).
## Scripts
| Script | Purpose |
|--------|---------|
| [`mapping.txt`](mapping.txt) | Source of truth: TCP port ↔ ID_PATH ↔ name ↔ baud |
| [`generate-config.sh`](generate-config.sh) | Generates udev rules, ser2net.yaml, conman.conf from mapping.txt |
| [`setup.sh`](setup.sh) | Full deploy: generate configs, create symlinks, restart services |
| [`discover.sh`](discover.sh) | Read-only discovery of USB adapters, existing config, services |
## Usage
### Connect to a console
**Primary method — conman client (with logging + multiplexing):**
```bash
# From any Tailscale-connected workstation:
conman -d pfv-tsys4:7890 -f pfv-core-sw01 # connect to console
conman -d pfv-tsys4:7890 -q # list all consoles
```
Escape sequence: `&.` to disconnect, `&?` for help.
**Direct telnet (emergency only — conflicts with conman):**
```bash
# Direct telnet to ser2net works ONLY when conmand is stopped, because
# conmand maintains persistent connections to all 7 TCP ports. Use:
ssh pfv-tsys4 'systemctl stop conmand'
telnet pfv-tsys4 2001 # pfv-core-sw01
ssh pfv-tsys4 'systemctl start conmand' # restart when done
```
**Do NOT use telnet while conmand is running** — conmand will reconnect
and kick your telnet session immediately ("Connection closed by foreign host").
The correct workflow is conman client → conmand → ser2net → device.
### Re-deploy after changing mapping.txt
```bash
PROX_HOST=pfv-tsys4 bash tests/remote.sh prox 'bash /root/console/setup.sh'
```
### Find the ID_PATH for a new adapter
```bash
PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/discover.sh
```
Then match the new adapter's ID_PATH to its physical location and add a line
to `mapping.txt`.
## Files on pfv-tsys4
| File | Purpose |
|------|---------|
| `/etc/udev/rules.d/99-console-ports.rules` | Stable symlinks by ID_PATH |
| `/etc/ser2net.yaml` | ser2net config (TCP ports → serial symlinks) |
| `/etc/conman.conf` | conman config (CONSOLE entries between markers) |
| `/etc/systemd/system/conmand.service` | systemd unit for conmand |
| `/root/console/mapping.txt` | Copy of the source-of-truth mapping |
| `/root/console/setup.sh` | Setup script (re-runnable) |
| `/root/console/generate-config.sh` | Config generator |
## Old workflow (replaced)
The old `/root/conmap` file and manual `screen` sessions are no longer
needed. The new setup is fully automated and survives reboots.
@@ -1,5 +1,4 @@
#!/usr/bin/bash
# shellcheck disable=SC2010,SC2012 # diagnostic script; ls|grep/ls -la on sysfs & log dirs is intentional for human-readable output
#
# console/discover.sh — READ-ONLY discovery of console setup on pfv-tsys4
#
@@ -6,7 +6,7 @@
# config files. This is the fix for the USB enumeration shift problem:
#
# 1. udev rules pin each adapter by its STABLE ID_PATH (physical USB port)
# to a named symlink like /dev/consoles/pfv-r5-core-01
# to a named symlink like /dev/consoles/pfv-core-sw01
# 2. ser2net opens those stable symlinks and exposes them on TCP ports
# (2001, 2002, ...) bound to the Tailscale IP
# 3. conman connects to those TCP ports for logging + multiplexing
@@ -33,6 +33,8 @@ CONMAN_LOGDIR="${CONMAN_LOGDIR:-/var/log/conman}"
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
SER2NET_CONF="/etc/ser2net.yaml"
CONMAN_CONF="/etc/conman.conf"
CONMAN_CONSOLES="/etc/conman/console-consoles.conf"
CONSOLE_DEV_DIR="/dev/console"
echo "============================================"
echo " Console Config Generator"
@@ -118,11 +120,9 @@ for entry in "${ENTRIES[@]}"; do
# Build the full ID_PATH match. The mapping stores a substring like "usb-0:1.5.4.4"
# The actual ID_PATH is like "pci-0000:00:1a.0-usb-0:1.5.4.4:1.0"
# We match on the substring to be portable across PCI bus changes.
{
echo ""
echo "# $name (TCP $tcp_port): $comment"
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"consoles/$name\""
} >> "$UDEV_RULES"
echo "" >> "$UDEV_RULES"
echo "# $name (TCP $tcp_port): $comment" >> "$UDEV_RULES"
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\"" >> "$UDEV_RULES"
done
echo " Written: $UDEV_RULES"
@@ -146,24 +146,20 @@ fi
echo "# ser2net configuration for pfv-tsys4 console ports"
echo "# Generated by console/generate-config.sh on $(date)"
echo "#"
echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients"
echo "# negotiate proper telnet binary mode — this prevents CR stripping"
printf '%s\n' "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings."
echo "# Ports bound to Tailscale IP ($TS_IP) for secure remote access."
echo "# All ports bound to Tailscale IP ($TS_IP) for secure remote access."
echo "# Physical devices are accessed via stable udev symlinks in /dev/consoles/."
echo "#"
echo "# Direct telnet: telnet $TS_IP 2001"
echo "# Via conman: conman -f <name>"
echo "# To connect directly: telnet $TS_IP 2001"
echo "# To connect via conman: conman -f <name>"
echo ""
printf '%s\n' "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n"
echo "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n"
echo ""
for entry in "${ENTRIES[@]}"; do
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
# ser2net connection block — telnet(rfc2217) accepter so conman and
# telnet clients negotiate proper telnet binary mode. This prevents
# CR stripping that occurs with raw TCP + conman's telnet NVT.
# ser2net connection block
echo "connection: &con${tcp_port}"
echo " accepter: telnet(rfc2217),tcp,${TS_IP},${tcp_port}"
echo " accepter: tcp,${TS_IP},${tcp_port}"
echo " enable: on"
echo " options:"
echo " banner: *banner"
@@ -220,8 +216,8 @@ fi
echo "$MARKER_BEGIN"
echo "# Generated by console/generate-config.sh on $(date)"
echo "# Each console connects to a ser2net TCP port via telnet protocol."
echo "# ser2net uses telnet(rfc2217) accepter so binary mode is negotiated"
echo "# and CR/LF translation is handled correctly by the telnet NVT layer."
echo "# ser2net owns the physical serial device; conman provides logging"
echo "# and multiplexing on top."
echo "# Access: conman -f <name>"
echo ""
for entry in "${ENTRIES[@]}"; do
@@ -17,13 +17,13 @@
# 2. Update the id_path_substring in this file
# 3. Run: bash console/generate-config.sh && udevadm trigger && systemctl restart ser2net conmand
#
2001|pfv-r5-core-01|usb-0:1.5.4.4|9600n81|Dell PowerConnect 5448 (rack 5 core switch)
2002|pfv-r3-tor-mgmt-01|usb-0:1.6.3.1|9600n81|Rack 3 management TOR switch
2003|pfv-r3-tor-stor-01|usb-0:1.6.3.3.2|9600n81|Rack 3 storage TOR switch
2001|pfv-core-sw01|usb-0:1.5.4.4|9600n81|Dell PowerConnect 5448 (core switch)
2002|pfv-tor3-mgmt|usb-0:1.6.3.1|9600n81|Rack 3 management TOR switch
2003|pfv-tor3-stor|usb-0:1.6.3.3.2|9600n81|Rack 3 storage TOR switch
2004|pfv-rrinfra-rtr|usb-0:1.6.3.3.1|9600n81|Cisco router (rrinfra)
2005|pfv-r2-tor-01|usb-0:1.6.3.3.3|9600n81|Rack 2 TOR switch
2006|pfv-r6-mgmt-01|usb-0:1.5.4.1|9600n81|Rack 6 management switch
# 2007|pfv-r2-sw|usb-0:1.6.3.2|9600n81|Rack 2 old Dell switch (dead, removed)
2005|pfv-r2-tor-top|usb-0:1.6.3.3.3|9600n81|Rack 2 top-of-rack switch
2006|subodev-torsw|usb-0:1.5.4.1|9600n81|Suborbital device TOR switch
2007|pfv-r2-sw|usb-0:1.6.3.2|9600n81|Rack 2 old Dell switch
# Unassigned (no device detected):
# 2008|spare-1|usb-0:1.6.3.4|9600n81|Empty / spare
# 2009|spare-2|usb-0:1.6.3.3.4|9600n81|Empty / spare
@@ -5,7 +5,7 @@
#
# Usage:
# bash console/query-remote.sh # list consoles
# bash console/query-remote.sh pfv-r5-core-01 # connect to a console
# bash console/query-remote.sh pfv-core-sw01 # connect to a console
#
set -euo pipefail
@@ -52,7 +52,7 @@ if [ -z "$CONSOLE" ]; then
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -q
echo ""
echo "To connect: bash $0 <console-name>"
echo " e.g: bash $0 pfv-r5-core-01"
echo " e.g: bash $0 pfv-core-sw01"
else
echo ""
echo "--- Connecting to: $CONSOLE ---"
+14 -15
View File
@@ -1,5 +1,4 @@
#!/usr/bin/bash
# shellcheck disable=SC2010 # diagnostic; ls|grep on /dev listing is intentional
#
# console/setup.sh — deploy console management on pfv-tsys4
#
@@ -20,6 +19,10 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
SER2NET_CONF="/etc/ser2net.yaml"
CONMAN_CONSOLES="/etc/conman/console-consoles.conf"
CONSOLE_DEV_DIR="/dev/consoles"
echo "============================================"
echo " Console Management Setup"
@@ -97,7 +100,7 @@ if [ ! -d /dev/consoles ] || [ -z "$(ls /dev/consoles/ 2>/dev/null)" ]; then
line="${line%%#*}"
line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue
IFS='|' read -r _ name id_path _ _ <<< "$line"
IFS='|' read -r tcp_port name id_path baud comment <<< "$line"
# Find the ttyUSB whose ID_PATH contains the mapping's id_path substring
for tty in /dev/ttyUSB*; do
[ -e "$tty" ] || continue
@@ -121,7 +124,7 @@ while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue
IFS='|' read -r _ name id_path _ _ <<< "$line"
IFS='|' read -r tcp_port name id_path baud comment <<< "$line"
if [ -e "/dev/consoles/$name" ]; then
TARGET=$(readlink -f "/dev/consoles/$name")
echo " [OK] /dev/consoles/$name -> $TARGET"
@@ -138,7 +141,7 @@ systemctl restart ser2net
sleep 2
if systemctl is-active --quiet ser2net; then
echo " ser2net is running (telnet rfc2217 accepters)."
echo " ser2net is running."
TS_IP=$(tailscale ip -4 2>/dev/null || echo "127.0.0.1")
echo " Listening ports:"
ss -tlnp | grep ser2net | grep -oE "${TS_IP}:[0-9]+" | sort -t: -k2 -n | sed 's/^/ /'
@@ -197,18 +200,14 @@ fi
echo ""
echo "--- [7/7] Setup complete ---"
echo ""
echo " ser2net + conman architecture (telnet rfc2217):"
echo " ser2net owns serial devices, exposes telnet(rfc2217) TCP ports"
echo " conman connects via telnet for logging + multiplexing"
echo " ser2net TCP ports (connect directly):"
echo " telnet <tailscale-ip> 2001 # pfv-core-sw01"
echo " telnet <tailscale-ip> 2002 # pfv-tor3-mgmt"
echo " ..."
echo ""
echo " Connect from any Tailscale workstation:"
echo " conman -d pfv-tsys4:7890 -f pfv-r5-core-01"
echo " conman -d pfv-tsys4:7890 -q # list consoles"
echo ""
echo " Direct telnet (emergency, conflicts with conman):"
echo " ssh pfv-tsys4 'systemctl stop conmand'"
echo " telnet pfv-tsys4 2001"
echo " ssh pfv-tsys4 'systemctl start conmand'"
echo " conman consoles (with logging):"
echo " conman -f pfv-core-sw01"
echo " conman -q # query status"
echo ""
echo " To regenerate after changing mapping.txt:"
echo " bash generate-config.sh"
@@ -1,5 +1,4 @@
#!/usr/bin/bash
# shellcheck disable=SC2012,SC2001 # diagnostic script; ls -la listings and sed line-prefixing are intentional
#
# console/validate-conman.sh — verify conman can actually reach devices via
# ser2net TCP ports and is capturing log output to files.
@@ -45,7 +44,7 @@ echo "--- 3. Trigger log capture: connect to each console briefly ---"
echo " conmand connects to all consoles on startup. Checking if logs exist..."
echo ""
echo "--- 4. Log file inventory ---"
for name in pfv-r5-core-01 pfv-r3-tor-mgmt-01 pfv-r3-tor-stor-01 pfv-rrinfra-rtr pfv-r2-tor-01 pfv-r6-mgmt-01; do
for name in pfv-core-sw01 pfv-tor3-mgmt pfv-tor3-stor pfv-rrinfra-rtr pfv-r2-tor-top subodev-torsw pfv-r2-sw; do
logfile="$LOGDIR/${name}.log"
if [ -f "$logfile" ]; then
SIZE=$(stat -c%s "$logfile" 2>/dev/null || echo 0)
@@ -62,7 +61,8 @@ echo "--- 5. conmand connection status (journal) ---"
journalctl -u conmand --no-pager -n 50 2>/dev/null | grep -iE "connect|error|fail|console|refused|timeout" | tail -15 || echo " (no relevant journal entries)"
echo ""
echo "--- 6. Verify ser2net is proxying data (telnet rfc2217) ---"
echo "--- 6. Verify ser2net is actually proxying data (check for byte flow) ---"
# Pick a known-active port (2007 = pfv-r2-sw, the old Dell with menu UI that responded)
echo " Probing TCP $TS_IP:2007 for data..."
RESPONSE=$(timeout 3 bash -c "printf '\r\r' | nc -w 2 $TS_IP 2007 2>/dev/null" | tr -cd '[:print:][:space:]' | head -5)
if [ -n "$RESPONSE" ]; then
-10
View File
@@ -1,10 +0,0 @@
# dcinfra/console/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Serial console management (ser2net + conman)**
>
> **Read it here:** https://community.turnsys.com/t/301
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-10
View File
@@ -1,10 +0,0 @@
# dcinfra/powerman/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Cyclades PM10i PDU management via powerman**
>
> **Read it here:** https://community.turnsys.com/t/301
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/bash
# powerman/identify-outlets.sh — flash each PDU outlet sequentially for physical tracing
#
# Run this from the workstation. It flashes each outlet one at a time so you
# can walk the rack and see which device's LED blinks. Write down the mapping,
# then run rename-outlets.sh with that mapping.
#
# Usage:
# bash dcinfra/powerman/identify-outlets.sh
#
# On Friday: run this, walk the rack, note which outlet → which device.
set -uo pipefail
PROX_HOST="${PROX_HOST:-pfv-tsys1}"
REMOTE_SH="$(cd "$(dirname "$0")/../.." && pwd)/tests/remote.sh"
echo "PDU Outlet Identification — Flash Sequence"
echo "============================================"
echo "Each outlet will flash for 5 seconds. Walk the rack and note the device."
echo "Press Enter to start..."
read -r
for i in $(seq 1 10); do
echo "--- Outlet $i: FLASHING (5s) ---"
PROX_HOST="$PROX_HOST" bash "$REMOTE_SH" prox "powerman -f outlet-$i" </dev/null 2>/dev/null
sleep 5
PROX_HOST="$PROX_HOST" bash "$REMOTE_SH" prox "powerman -u outlet-$i" </dev/null 2>/dev/null
echo " Outlet $i → ? (write it down)"
echo ""
[ "$i" -lt 10 ] && { echo "Press Enter for next outlet..."; read -r; }
done
echo "============================================"
echo "Done. Now create your mapping file and run:"
echo " bash dcinfra/powerman/rename-outlets.sh"
echo ""
echo "Format: outlet-number:new-name (one per line)"
echo "Example:"
echo " 1:pfv-tsys1"
echo " 2:pfv-tsys3"
echo " ..."
echo "============================================"
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/bash
# powerman/rename-outlets.sh — rename PDU outlets in powerman.conf
#
# Takes a mapping file (outlet-number:new-name, one per line) and rewrites
# the node entries in /etc/powerman/powerman.conf on pfv-tsys1, then
# restarts powermand.
#
# Usage:
# bash dcinfra/powerman/rename-outlets.sh <mapping-file>
#
# Example mapping file:
# 1:pfv-tsys1
# 2:pfv-tsys3
# 3:pfv-tsys4
# ...
set -euo pipefail
PROX_HOST="${PROX_HOST:-pfv-tsys1}"
REMOTE_SH="$(cd "$(dirname "$0")/../.." && pwd)/tests/remote.sh"
MAP_FILE="${1:-}"
if [ -z "$MAP_FILE" ] || [ ! -f "$MAP_FILE" ]; then
echo "Usage: $0 <mapping-file>"
echo " Format: outlet-number:new-name (one per line)"
echo " Run identify-outlets.sh first to get the mapping."
exit 1
fi
# Build the new node lines
NODE_LINES=""
while IFS=: read -r num name; do
[ -z "$num" ] && continue
NODE_LINES+="node \"$name\" \"cyclades-pm10\" \"$num\""$'\n'
done < "$MAP_FILE"
# Send to tsys1: backup conf, write new node section, restart powermand
PROX_HOST="$PROX_HOST" bash "$REMOTE_SH" prox-file - <<REMOTE_SCRIPT
set -euo pipefail
cp /etc/powerman/powerman.conf /etc/powerman/powerman.conf.bak.\$(date +%Y%m%d-%H%M%S)
# Strip existing node lines and append new ones
grep -v '^node "outlet-' /etc/powerman/powerman.conf > /tmp/powerman.conf.new
cat >> /tmp/powerman.conf.new <<'NODES'
$(echo -n "$NODE_LINES")
NODES
mv /tmp/powerman.conf.new /etc/powerman/powerman.conf
systemctl restart powerman
sleep 1
powerman -l
REMOTE_SCRIPT
echo "PDU outlets renamed. Verify with: PROX_HOST=$PROX_HOST bash $REMOTE_SH prox 'powerman -q'"
-10
View File
@@ -1,10 +0,0 @@
# dcinfra/ups/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **UPS management (NUT) for APC Smart-UPS C 1500**
>
> **Read it here:** https://community.turnsys.com/t/301
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/bash
#
# ups/discover.sh — probe USB UPS units and NUT state on the local host
#
# Read-only. Prints everything needed to configure NUT. No changes made.
#
# Usage (run ON the target host via remote.sh):
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/discover.sh
#
set -uo pipefail
echo "======================================================"
echo " UPS / NUT Discovery on $(hostname)"
echo "======================================================"
# --- 1. USB UPS devices ------------------------------------------------------
echo ""
echo "--- [1/5] USB UPS devices (lsusb) ---"
lsusb 2>/dev/null | grep -iE "UPS|American Power|Tripp|APC" || echo " (no UPS devices found in lsusb)"
echo ""
echo "--- [2/5] UPS detail (vendor/product/serial/model) ---"
# Common UPS vendor IDs: 051d (APC), 09ae (Tripp Lite), 0463 (Eaton),
# 06da (MGE), 0764 (Cyber Power)
for vid in 051d 09ae 0463 06da 0764; do
while read -r bus dev pid; do
[ -n "$bus" ] || continue
echo " --- $bus:$dev ($vid:$pid) ---"
lsusb -v -s "${bus}:${dev}" 2>/dev/null \
| grep -iE "iManufacturer|iProduct|iSerial|bcdDevice" \
| sed 's/^/ /'
done < <(lsusb 2>/dev/null | awk -v v="$vid" '$0~v{split($2,a,":"); split($4,b,":"); print a[1], b[1], $6}')
done
# --- 2. sysfs paths (for udev rules) -----------------------------------------
echo ""
echo "--- [3/5] sysfs device paths + serials ---"
for d in /sys/bus/usb/devices/*; do
man=$(cat "$d/manufacturer" 2>/dev/null)
prod=$(cat "$d/product" 2>/dev/null)
ser=$(cat "$d/serial" 2>/dev/null)
vid=$(cat "$d/idVendor" 2>/dev/null)
pid=$(cat "$d/idProduct" 2>/dev/null)
if echo "$man $prod" | grep -qiE "apc|tripp|power conversion|ups|eaton|mge|cyber power"; then
# Resolve stable ID_PATH for udev pinning
path=$(udevadm info -q property -p "$d" 2>/dev/null | awk -F= '/^ID_PATH=/{print $2}')
echo " $d"
echo " vendor=$vid product=$pid"
echo " manufacturer=$man"
echo " product=$prod"
echo " serial=$ser"
echo " ID_PATH=$path"
fi
done
# --- 3. HID device nodes -----------------------------------------------------
echo ""
echo "--- [4/5] HID device nodes ---"
ls -la /dev/hidraw* /dev/usb/hiddev* 2>/dev/null || echo " (no hidraw/hiddev nodes)"
# --- 4. NUT install state ----------------------------------------------------
echo ""
echo "--- [5/5] NUT install + service state ---"
if dpkg -l nut-server nut-client 2>/dev/null | grep -q '^ii'; then
echo " NUT installed:"
dpkg -l nut-server nut-client 2>/dev/null | awk '/^ii/{print " "$2" "$3}'
else
echo " NUT not installed (apt: nut-server nut-client)"
fi
echo ""
echo " Services:"
for svc in nut-driver nut-server nut-monitor; do
printf " %-14s " "$svc:"
systemctl is-active "$svc" 2>/dev/null || true
done
echo ""
echo " Existing config:"
# shellcheck disable=SC2012 # ls -la is intentional for human-readable listing
ls -la /etc/nut/ 2>/dev/null | sed 's/^/ /' || echo " (no /etc/nut)"
echo ""
echo "======================================================"
echo " Discovery complete."
echo "======================================================"
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""
ha-nut-setup.py — Add the Home Assistant NUT integration via REST config-flow API.
Stdlib-only (no pip). Idempotent: skips if a NUT config entry already exists.
Env:
HA_HOST (default pfv-bms.knel.net)
HA_PORT (default 8123)
HA_TOKEN (long-lived access token)
NUT_HOST (default 100.121.189.98)
NUT_PORT (default 3493)
NUT_USER (default homeassistant)
NUT_PASS (required)
NUT_UPS (default apc-smartups-c1500)
"""
import os, json, sys, time, urllib.request, urllib.error
HA_HOST = os.environ.get("HA_HOST", "pfv-bms.knel.net")
HA_PORT = int(os.environ.get("HA_PORT", "8123"))
TOKEN = os.environ["HA_TOKEN"]
NUT_HOST = os.environ.get("NUT_HOST", "192.168.3.11")
NUT_PORT = int(os.environ.get("NUT_PORT", "3493"))
NUT_USER = os.environ.get("NUT_USER", "homeassistant")
NUT_PASS = os.environ["NUT_PASS"]
NUT_UPS = os.environ.get("NUT_UPS", "apc-smartups-c1500")
BASE = f"http://{HA_HOST}:{HA_PORT}"
def api(method, path, data=None):
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(
f"{BASE}/api{path}", data=body, method=method,
headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=20) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
raw = e.read().decode()
try:
return json.loads(raw)
except Exception:
return {"_http_error": e.code, "_raw": raw[:300]}
except Exception as e:
return {"_error": str(e)}
# ── verify token ──
cfg = api("GET", "/config")
if "_http_error" in cfg or "_error" in cfg:
print(f"Cannot reach HA or token invalid: {cfg}"); sys.exit(1)
print(f"HA {cfg.get('version')} — token valid")
# ── check existing entries (idempotent) ──
entries = api("GET", "/config/config_entries/entry")
existing = [e for e in entries if e.get("domain") == "nut"]
if existing:
for e in existing:
print(f"NUT already configured: {e.get('title')} "
f"(data={json.dumps(e.get('data', {}))})")
print("Skipping — delete it in HA UI first if you want to re-run.")
sys.exit(0)
print("No existing NUT entry. Starting config flow.")
# ── initiate flow ──
flow = api("POST", "/config/config_entries/flow", {"handler": "nut"})
if "flow_id" not in flow:
print(f"Flow init failed: {json.dumps(flow)}"); sys.exit(1)
fid = flow["flow_id"]
print(f"Flow started: step={flow.get('step_id')} "
f"fields={[f.get('name') for f in flow.get('data_schema', [])]}")
# ── submit connection details ──
creds = {"host": NUT_HOST, "port": NUT_PORT,
"username": NUT_USER, "password": NUT_PASS}
flow = api("POST", f"/config/config_entries/flow/{fid}", creds)
if flow.get("errors"):
print(f"Validation errors: {flow['errors']}"); sys.exit(1)
print(f"After submit: type={flow.get('type')} step={flow.get('step_id')}")
# ── handle follow-up steps (UPS selection etc.) ──
while flow.get("type") == "form":
step = flow.get("step_id", "?")
schema = flow.get("data_schema", [])
print(f"Step '{step}': fields={[f.get('name') for f in schema]}")
for f in schema:
opts = f.get("options") or f.get("values")
if opts:
print(f" {f.get('name')} options: {opts}")
submission = {}
for f in schema:
nm = f.get("name")
ftype = f.get("type", "")
if ftype == "multi_select":
opts = f.get("options", [])
vals = [o[0] if isinstance(o, list) else o for o in opts]
submission[nm] = [NUT_UPS] if NUT_UPS in vals else vals[:1]
elif nm in creds:
submission[nm] = creds[nm]
elif "default" in f:
submission[nm] = f["default"]
elif ftype == "select":
opts = f.get("options", [])
vals = [o[0] if isinstance(o, list) else o for o in opts]
submission[nm] = NUT_UPS if NUT_UPS in vals else (vals[0] if vals else "")
fid = flow.get("flow_id", fid)
flow = api("POST", f"/config/config_entries/flow/{fid}", submission)
if flow.get("errors"):
print(f"Validation errors: {flow['errors']}"); sys.exit(1)
print(f" -> type={flow.get('type')} step={flow.get('step_id')}")
# ── result ──
if flow.get("type") == "create_entry":
print(f"\nNUT integration created: {flow.get('title')}")
elif flow.get("type") == "abort":
print(f"\nFlow aborted: {flow.get('reason')}"); sys.exit(1)
else:
print(f"\nFinal state: {flow.get('type')}{json.dumps(flow)[:200]}")
# ── verify sensors ──
print("\nWaiting 10s for entities ...")
time.sleep(10)
states = api("GET", "/states")
ups = [s for s in states
if "apc_smartups" in s["entity_id"].lower()
or "sensor.ups_" in s["entity_id"].lower()]
if ups:
print(f"Found {len(ups)} UPS sensors:")
for e in sorted(ups, key=lambda x: x["entity_id"]):
st = e.get("state", "?")
unit = e.get("attributes", {}).get("unit_of_measurement", "")
name = e.get("attributes", {}).get("friendly_name", "")
print(f" {e['entity_id']:55s} {st:>8} {unit:4s} {name}")
else:
print("No UPS sensors yet (may still be initialising — check HA UI).")
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
#
# setup-ha-nut.sh — Add the Home Assistant NUT integration via REST API.
#
# Idempotent: skips if a NUT entry already exists. Reads secrets from
# ~/.config/pfvcluster/ (ha-token, nut-password) or env vars.
#
# Usage:
# bash ups/setup-ha-nut.sh
#
# Env overrides:
# HA_TOKEN HA long-lived access token
# NUT_PASS NUT upsd password for the homeassistant user
# HA_HOST HA host (default pfv-bms.knel.net)
# NUT_HOST upsd host (default 192.168.3.11 — LAN, see README)
# NUT_PORT upsd port (default 3493)
# NUT_USER upsd user (default homeassistant)
# NUT_UPS UPS name (default apc-smartups-c1500)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_DIR="${PFV_CONF_DIR:-$HOME/.config/pfvcluster}"
# --- HA token ---
HA_TOKEN="${HA_TOKEN:-}"
if [[ -z "$HA_TOKEN" ]]; then
TOKEN_FILE="$CONF_DIR/ha-token"
if [[ -f "$TOKEN_FILE" ]]; then
HA_TOKEN="$(head -1 "$TOKEN_FILE" | tr -d '[:space:]')"
else
echo "error: no HA token. Set \$HA_TOKEN or create $TOKEN_FILE" >&2
echo " (HA → Profile → Long-Lived Access Tokens → Create Token)" >&2
exit 1
fi
fi
# --- NUT password ---
NUT_PASS="${NUT_PASS:-}"
if [[ -z "$NUT_PASS" ]]; then
PASS_FILE="$CONF_DIR/nut-password"
if [[ -f "$PASS_FILE" ]]; then
NUT_PASS="$(head -1 "$PASS_FILE" | tr -d '[:space:]')"
else
echo "error: no NUT password. Set \$NUT_PASS or create $PASS_FILE" >&2
echo " (value is in /etc/nut/upsd.users on the NUT host)" >&2
exit 1
fi
fi
# --- connection params (override for your own kit) ---
export HA_TOKEN
export NUT_PASS
export HA_HOST="${HA_HOST:-pfv-bms.knel.net}"
export HA_PORT="${HA_PORT:-8123}"
export NUT_HOST="${NUT_HOST:-192.168.3.11}"
export NUT_PORT="${NUT_PORT:-3493}"
export NUT_USER="${NUT_USER:-homeassistant}"
export NUT_UPS="${NUT_UPS:-apc-smartups-c1500}"
echo "HA: ${HA_HOST}:${HA_PORT}"
echo "NUT: ${NUT_USER}@${NUT_HOST}:${NUT_PORT} (UPS: ${NUT_UPS})"
echo ""
exec python3 "$SCRIPT_DIR/ha-nut-setup.py"
-298
View File
@@ -1,298 +0,0 @@
#!/usr/bin/bash
#
# ups/setup.sh — idempotent Network UPS Tools (NUT) setup on pfv-tsys1
#
# Installs NUT, configures two USB HID UPS units (APC + Tripp Lite) pinned by
# USB serial, runs upsd as a network server for Home Assistant polling, and
# runs upsmon locally so the hypervisor can shut down gracefully on battery.
#
# Designed to run ON the target host (pfv-tsys1) as root, idempotent.
#
# Usage:
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/setup.sh
#
# Overrides (defaults suit pfv-tsys1):
# APC_SERIAL APC UPS USB serial (default AS1213210423)
# APC_VID/APC_PID APC vendor/product ID (default 051d / 0003)
# APC_NAME NUT section name for APC (default apc-smartups-c1500)
# TRIPP_SERIAL Tripp Lite UPS USB serial (default 2352CVLSM871900694)
# TRIPP_VID/TRIPP_PID Tripp Lite vendor/product (default 09ae / 3016)
# TRIPP_NAME NUT section name for Tripp (default tripp-lite-ups)
# TRIPP_SUBDRIVER Forced HID subdriver for Tripp (default "TrippLite HID 0.85")
# TRIPP_ENABLED Set to 0 to disable Tripp Lite (default 1)
# NUT_LISTEN_IPS space-separated upsd LISTEN IPs (default: auto Tailscale + 127.0.0.1)
# HA_USER upsd username for HA (default homeassistant)
# HA_PASSWORD upsd password for HA (default: reuse or generate)
# MON_USER upsd username for local upsmon (default monuser)
# MON_PASSWORD upsd password for upsmon (default: reuse or generate)
#
set -euo pipefail
# --- Config (overridable via env) ---
APC_SERIAL="${APC_SERIAL:-AS1213210423}"
APC_VID="${APC_VID:-051d}"
APC_PID="${APC_PID:-0003}"
APC_NAME="${APC_NAME:-apc-smartups-c1500}"
TRIPP_SERIAL="${TRIPP_SERIAL:-2352CVLSM871900694}"
TRIPP_VID="${TRIPP_VID:-09ae}"
TRIPP_PID="${TRIPP_PID:-3016}"
TRIPP_NAME="${TRIPP_NAME:-tripp-lite-ups}"
TRIPP_SUBDRIVER="${TRIPP_SUBDRIVER:-TrippLite HID 0.85}"
TRIPP_ENABLED="${TRIPP_ENABLED:-1}"
HA_USER="${HA_USER:-homeassistant}"
MON_USER="${MON_USER:-monuser}"
NUT_PORT="${NUT_PORT:-3493}"
UDEV_RULE="/etc/udev/rules.d/99-nut-ups.rules"
UPS_CONF="/etc/nut/ups.conf"
UPSD_CONF="/etc/nut/upsd.conf"
UPSD_USERS="/etc/nut/upsd.users"
UPS_CONF_MON="/etc/nut/upsmon.conf"
NUT_CONF="/etc/nut/nut.conf"
# --- Helpers ---
gen_pw() { head -c 24 /dev/urandom | base64 | tr -d '/+=' | cut -c1-20; }
# Reuse existing passwords if config already present (idempotent re-runs)
extract_pw() { # $1=user
if [ -f "$UPSD_USERS" ]; then
awk -v u="[$1]" '
$0==u {inblk=1; next}
/^\[/ {inblk=0}
inblk && $1=="password" {gsub(/"/,"",$3); print $3; exit}
' "$UPSD_USERS" 2>/dev/null
fi
}
echo "============================================"
echo " NUT UPS Setup on $(hostname)"
echo " APC: $APC_NAME ($APC_VID:$APC_PID serial $APC_SERIAL)"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo " Tripp Lite: $TRIPP_NAME ($TRIPP_VID:$TRIPP_PID serial $TRIPP_SERIAL)"
else
echo " Tripp Lite: DISABLED (TRIPP_ENABLED=0)"
fi
echo "============================================"
# --- 0. Resolve / generate passwords (idempotent) ---
MON_PASSWORD="${MON_PASSWORD:-$(extract_pw "$MON_USER")}"
HA_PASSWORD="${HA_PASSWORD:-$(extract_pw "$HA_USER")}"
[ -n "$MON_PASSWORD" ] || MON_PASSWORD="$(gen_pw)"
[ -n "$HA_PASSWORD" ] || HA_PASSWORD="$(gen_pw)"
# --- 0b. Auto-detect listen IPs for upsd ---
# Tailscale IP: tailnet clients (workstation, etc.)
# LAN IP: HAOS VMs where Tailscale runs as an isolated add-on (HA container
# cannot route to Tailscale IPs, so the shared-LAN bridge is required)
if [ -z "${NUT_LISTEN_IPS:-}" ]; then
NUT_LISTEN_IPS="127.0.0.1"
TS_IP=$(tailscale ip -4 2>/dev/null || true)
if [ -n "$TS_IP" ]; then
NUT_LISTEN_IPS="${NUT_LISTEN_IPS} ${TS_IP}"
else
echo " WARNING: No Tailscale IP detected."
fi
if [ "${NUT_INCLUDE_LAN:-1}" = "1" ]; then
LAN_IP=$(ip -4 addr show vmbr0 2>/dev/null | awk '/scope global/{print $2}' | cut -d/ -f1 | head -1)
if [ -z "$LAN_IP" ]; then
LAN_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
if [ -n "$LAN_IP" ]; then
NUT_LISTEN_IPS="${NUT_LISTEN_IPS} ${LAN_IP}"
fi
fi
fi
echo " upsd LISTEN IPs: ${NUT_LISTEN_IPS:-<none>}"
# --- 1. Install NUT ---
echo ""
echo "--- [1/8] Installing NUT (nut-server, nut-client) ---"
if dpkg -l nut-server 2>/dev/null | grep -q '^ii'; then
echo " NUT already installed: $(dpkg -l nut-server | awk '/^ii/{print $3}')"
else
apt-get update -qq && apt-get install -y -qq nut-server nut-client
fi
mkdir -p /etc/nut
# --- 2. udev rules: grant nut group access to BOTH raw USB + hidraw devices ---
# CRITICAL: usbhid-ups opens /dev/bus/usb/BBB/DDD (raw USB), not /dev/hidraw.
# The driver drops to the nut user via setuid(), so the nut group needs write
# access to the raw USB device files. Matching on subsystem=="usb" by VID:PID
# is required because ATTRS{serial} does not reliably traverse for usb devices.
echo ""
echo "--- [2/8] Writing udev rules (raw USB + hidraw, group nut) ---"
{
echo "# Stable permissions for NUT USB HID UPS units"
echo "# Generated by ups/setup.sh — grants the 'nut' group access to both"
echo "# the raw USB device files (/dev/bus/usb) and hidraw devices."
echo "# Match BOTH subsystems: the usbhid-ups driver opens the raw USB device"
echo "# after dropping to the nut user via setuid()."
echo ""
echo "# APC Smart-UPS C 1500 ($APC_VID:$APC_PID)"
echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$APC_VID\", ATTR{idProduct}==\"$APC_PID\", GROUP=\"nut\", MODE=\"0664\""
echo "SUBSYSTEM==\"hidraw\", ATTRS{serial}==\"$APC_SERIAL\", GROUP=\"nut\", MODE=\"0660\""
echo ""
echo "# Tripp Lite UPS ($TRIPP_VID:$TRIPP_PID)"
echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$TRIPP_VID\", ATTR{idProduct}==\"$TRIPP_PID\", GROUP=\"nut\", MODE=\"0664\""
echo "SUBSYSTEM==\"hidraw\", ATTRS{serial}==\"$TRIPP_SERIAL\", GROUP=\"nut\", MODE=\"0660\""
} > "$UDEV_RULE"
echo " Written: $UDEV_RULE"
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=usb 2>/dev/null || true
udevadm trigger --subsystem-match=hidraw 2>/dev/null || true
sleep 1
# --- 3. ups.conf ---
echo ""
echo "--- [3/8] Writing ups.conf ---"
{
echo "# NUT UPS devices — generated by ups/setup.sh on $(date)"
echo ""
echo "maxretry = 3"
echo ""
echo "[${APC_NAME}]"
echo " driver = usbhid-ups"
echo " port = auto"
echo " vendorid = ${APC_VID}"
echo " productid = ${APC_PID}"
echo " serial = ${APC_SERIAL}"
echo " desc = \"APC Smart-UPS C 1500\""
if [ "$TRIPP_ENABLED" = "1" ]; then
echo ""
echo "[${TRIPP_NAME}]"
echo " driver = usbhid-ups"
echo " port = auto"
echo " vendorid = ${TRIPP_VID}"
echo " productid = ${TRIPP_PID}"
echo " serial = ${TRIPP_SERIAL}"
echo " subdriver = \"${TRIPP_SUBDRIVER}\""
echo " desc = \"Tripp Lite UPS\""
fi
} > "$UPS_CONF"
echo " Written: $UPS_CONF"
# --- 4. upsd.conf: network server (localhost + Tailscale for HA) ---
echo ""
echo "--- [4/8] Writing upsd.conf ---"
{
echo "# NUT upsd — generated by ups/setup.sh on $(date)"
for ip in $NUT_LISTEN_IPS; do
echo "LISTEN ${ip} ${NUT_PORT}"
done
echo "MAXAGE 25"
} > "$UPSD_CONF"
echo " Written: $UPSD_CONF (LISTEN: $(echo "$NUT_LISTEN_IPS" | tr '\n' ' '))"
# --- 5. upsd.users: monuser (master) + homeassistant (read-only monitor) ---
echo ""
echo "--- [5/8] Writing upsd.users ---"
cat > "$UPSD_USERS" <<USERS
# NUT upsd users — generated by ups/setup.sh on $(date)
# monuser : local upsmon (master) — graceful hypervisor shutdown
# ${HA_USER} : Home Assistant NUT integration (read-only polling)
[${MON_USER}]
password = "${MON_PASSWORD}"
upsmon master
[${HA_USER}]
password = "${HA_PASSWORD}"
upsmon slave
USERS
echo " Written: $UPSD_USERS"
# --- 6. upsmon.conf + nut.conf ---
echo ""
echo "--- [6/8] Writing upsmon.conf + nut.conf ---"
{
echo "# NUT upsmon (local monitor) — generated by ups/setup.sh on $(date)"
echo "# Monitors the APC UPS as master. On battery-low, shuts the host down."
echo ""
echo "MONITOR ${APC_NAME}@localhost 1 ${MON_USER} \"${MON_PASSWORD}\" master"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo "MONITOR ${TRIPP_NAME}@localhost 1 ${MON_USER} \"${MON_PASSWORD}\" master"
fi
echo ""
echo "SHUTDOWNCMD \"/sbin/shutdown -h now\""
echo "POWERDOWNFLAG /etc/killpower"
echo "NOTIFYFLAG ONLINE SYSLOG+WALL"
echo "NOTIFYFLAG ONBATT SYSLOG+WALL"
echo "NOTIFYFLAG LOWBATT SYSLOG+WALL"
echo "POLLFREQ 15"
echo "POLLFREQALERT 5"
echo "HOSTSYNC 15"
} > "$UPS_CONF_MON"
cat > "$NUT_CONF" <<NUTCONF
# NUT mode — generated by ups/setup.sh on $(date)
# netserver = this host runs drivers + upsd; serves UPS data to clients (HA).
MODE=netserver
NUTCONF
echo " Written: $UPS_CONF_MON + $NUT_CONF"
# --- 6b. Fix ownership/permissions (Debian: nut group must read config) ---
chown root:nut "$UPS_CONF" "$UPSD_CONF" "$UPSD_USERS" "$UPS_CONF_MON" 2>/dev/null || true
chmod 640 "$UPS_CONF" "$UPSD_CONF" "$UPSD_USERS" "$UPS_CONF_MON" 2>/dev/null || true
chmod 644 "$NUT_CONF" 2>/dev/null || true
# --- 7. Start services (Debian uses templated nut-driver@<name> units) ---
echo ""
echo "--- [7/8] Starting NUT services ---"
# Re-read ups.conf to generate per-UPS driver instances
systemctl restart nut-driver-enumerator 2>/dev/null || true
sleep 2
# Start per-UPS driver instances
systemctl restart "nut-driver@${APC_NAME}" 2>/dev/null || true
if [ "$TRIPP_ENABLED" = "1" ]; then
systemctl restart "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
else
systemctl stop "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
systemctl mask "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
fi
sleep 3
systemctl restart nut-server 2>/dev/null || true
sleep 1
systemctl restart nut-monitor 2>/dev/null || true
echo ""
echo " Service status:"
for svc in "nut-driver@${APC_NAME}" "nut-driver@${TRIPP_NAME}" nut-server nut-monitor; do
if systemctl list-unit-files "$svc" >/dev/null 2>&1; then
printf " %-42s " "$svc"
systemctl is-active "$svc" 2>/dev/null || echo "(unknown)"
fi
done
# --- 8. Validate ---
echo ""
echo "--- [8/8] Validation ---"
echo ""
echo " upsc — ${APC_NAME}:"
upsc "${APC_NAME}@localhost" 2>&1 | head -25 || echo " (APC UPS not responding yet)"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo ""
echo " upsc — ${TRIPP_NAME}:"
upsc "${TRIPP_NAME}@localhost" 2>&1 | head -25 || echo " (Tripp Lite UPS not responding yet)"
fi
echo ""
echo "============================================"
echo " Setup complete."
echo ""
echo " Home Assistant NUT integration:"
echo " Host: $(echo "$NUT_LISTEN_IPS" | awk '{print $2}') (or any LISTEN IP above)"
echo " Port: ${NUT_PORT}"
echo " Username: ${HA_USER}"
echo " Password: ${HA_PASSWORD}"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo " UPS names: ${APC_NAME}, ${TRIPP_NAME}"
else
echo " UPS names: ${APC_NAME}"
fi
echo ""
echo " Save the HA password now — it is stored in ${UPSD_USERS}."
echo "============================================"
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/bash
#
# ups/status.sh — query NUT UPS state + service health (read-only)
#
# Usage (run ON the target host via remote.sh):
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/status.sh
#
# shellcheck disable=SC2012 # ss/awk field extraction is intentional
set -uo pipefail
APC_NAME="${APC_NAME:-apc-smartups-c1500}"
TRIPP_NAME="${TRIPP_NAME:-tripp-lite-ups}"
echo "======================================================"
echo " NUT UPS Status on $(hostname)"
echo "======================================================"
echo ""
echo "--- Services ---"
for svc in "nut-driver@${APC_NAME}" "nut-driver@${TRIPP_NAME}" nut-server nut-monitor; do
if systemctl list-unit-files "$svc" >/dev/null 2>&1; then
printf " %-42s " "$svc"
systemctl is-active "$svc" 2>/dev/null || echo "(unknown)"
fi
done
for ups in "$APC_NAME" "$TRIPP_NAME"; do
echo ""
echo "--- ${ups} ---"
if upsc "${ups}@localhost" >/tmp/.nutstatus.$$ 2>&1; then
awk -v u="$ups" '
BEGIN{printf " %s\n", u}
/^battery\.charge:/ {printf " battery.charge: %s\n", $3}
/^battery\.runtime:/ {printf " battery.runtime: %ss (%.0f min)\n", $3, $3/60}
/^battery\.voltage:/ {printf " battery.voltage: %s\n", $3}
/^ups\.status:/ {printf " ups.status: %s\n", $3}
/^ups\.load:/ {printf " ups.load: %s%%\n", $3}
/^ups\.power:/ {printf " ups.power: %s\n", $3}
/^ups\.realpower:/ {printf " ups.realpower: %s W\n", $3}
/^input\.voltage:/ {printf " input.voltage: %s\n", $3}
/^output\.voltage:/ {printf " output.voltage: %s\n", $3}
/^ups\.model:/ {printf " ups.model: %s\n", $3}
/^ups\.serial:/ {printf " ups.serial: %s\n", $3}
/^device\.mfr:/ {printf " device.mfr: %s\n", $3}
' /tmp/.nutstatus.$$
echo " (full dump: upsc ${ups}@localhost)"
else
echo " NOT RESPONDING:"
sed 's/^/ /' /tmp/.nutstatus.$$
fi
rm -f /tmp/.nutstatus.$$
done
echo ""
echo "--- upsd LISTEN sockets ---"
ss -ltnp 2>/dev/null | grep -E "3493|nut" | sed 's/^/ /' || echo " (upsd not listening on 3493)"
echo ""
echo "======================================================"
+183
View File
@@ -0,0 +1,183 @@
# Technitium DNS Cluster Setup
Replicates the production Technitium DNS Server from `tailscale-router` to the
`pfv-netinfra-01/02` pair and configures them as a primary/secondary cluster
with automatic zone transfers.
## Architecture
```
tailscale-router (PRODUCTION — READ ONLY)
└─ tsys-dns container (technitium/dns-server)
└─ 124 zones (knel.net + reverse DNS)
└─ Users + 2FA in auth.config
docker cp (export)
┌─ pfv-netinfra-01 (192.168.3.252) ──── PRIMARY ──────────┐
│ tsys-dns container (Technitium on :5300) │
│ pihole container (Pi-hole on :53 → Technitium :5300) │
│ All zones are Primary │
│ Zone transfer allowed from 192.168.3.253 │
└──────────────────────────────────────────────────────────┘
AXFR / IXFR + NOTIFY (DNS zone transfer, port 5300)
┌─ pfv-netinfra-02 (192.168.3.253) ─── SECONDARY ────────┐
│ tsys-dns container (Technitium on :5300) │
│ pihole container (Pi-hole on :53 → Technitium :5300) │
│ All zones are Secondary (AXFR from 01) │
└──────────────────────────────────────────────────────────┘
```
### How clustering works
Technitium uses standard DNS zone transfers (AXFR/IXFR) for primary/secondary
replication, not a proprietary protocol:
1. **Primary (01)** holds all zones as authoritative primary zones.
2. **Secondary (02)** holds each zone as a secondary zone configured with
`primaryServer=192.168.3.252:5300`.
3. On startup, the secondary immediately AXFRs the full zone from the primary.
4. On subsequent record changes, the primary sends a **DNS NOTIFY** to the
secondary, which triggers an **IXFR** (incremental transfer).
5. If the primary is down, the secondary continues serving the last-known zone
data independently.
### Credentials and 2FA
The production `auth.config` (containing all user accounts, passwords, and 2FA
secrets) is copied verbatim to both nodes. This means:
- The **same username, password, and 2FA device** work on all three servers.
- The web console is at `http://<host>:5380/` on each node.
- No credential changes are needed.
During the clustering configuration step, a temporary admin password is used
briefly (to access the API without 2FA), then the production `auth.config` is
restored. See "Security notes" below.
## Prerequisites
- SSH key access to all hosts as `localuser` with passwordless sudo.
- The `remote-dns.sh` wrapper must be able to reach all hosts via Tailscale FQDN.
- Docker + Docker Compose on netinfra-01/02 (already installed).
- The production Technitium on tailscale-router must be running.
## Usage
```bash
cd dns-cluster-setup/
# Step-by-step (recommended for first run):
./setup.sh export # 1. Export config from tailscale-router (READ-ONLY)
./setup.sh deploy01 # 2. Deploy to netinfra-01 as primary
./setup.sh deploy02 # 3. Deploy to netinfra-02 as secondary clone
./setup.sh cluster # 4. Configure clustering (01→02 zone transfers)
./setup.sh verify # 5. Run all verification tests
# Or all at once:
./setup.sh all
```
### Configuration overrides
All defaults can be overridden via environment variables:
| Variable | Default | Description |
|---|---|---|
| `PRIMARY_IP` | `192.168.3.252` | netinfra-01 LAN IP |
| `SECONDARY_IP` | `192.168.3.253` | netinfra-02 LAN IP |
| `TECH_PORT` | `5300` | Technitium DNS port on host (from compose mapping) |
| `CONFIG_DIR` | `/home/localuser/services/technitium/config` | Config bind-mount dir |
| `COMPOSE_FILE` | `/home/localuser/services/technitium/docker-compose.yml` | Compose file |
| `TEMP_ADMIN_PW` | `KnelClusterSetup!2026` | Temp admin password (used only during clustering, then discarded) |
## Scripts
| Script | Purpose |
|---|---|
| `remote-dns.sh` | SSH/SCP chokepoint for all DNS host access (tsrouter, netinfra01, netinfra02, netboot, sandbox) |
| `setup.sh` | Master orchestrator: export → deploy → cluster → verify |
| `verify.sh` | Comprehensive 10-section verification suite |
| `discover*.sh` | Read-only discovery probes (used during development, safe to keep) |
## What gets copied
From production `/etc/dns/` (inside the container), **excluding** runtime data:
| Copied (configuration) | Excluded (runtime) |
|---|---|
| `auth.config` (users, passwords, 2FA) | `cache.bin` (DNS cache) |
| `dns.config` (server settings) | `stats/` (query statistics) |
| `webservice.config` (web console) | `logs/` (log files) |
| `allowed.config` (zone transfer ACL) | |
| `blocked.config` (blocked domains) | |
| `blocklist.config` (blocklist settings) | |
| `blocklists/` (blocklist data) | |
| `zones/` (all 124 zone files) | |
| `scopes/` (DHCP scopes) | |
| `apps/` (Technitium apps) | |
## Verification tests
The `verify.sh` script runs 10 categories of tests:
1. **Container health** — both Technitium containers are Up
2. **API responds** — web console API is reachable on both nodes
3. **Zone count** — primary matches production; secondary matches primary
4. **Forward DNS** — known knel.net records resolve identically on both nodes
5. **External DNS** — both nodes can resolve external domains (github.com)
6. **Zone transfer (AXFR)** — secondary can AXFR knel.net from primary
7. **Reverse DNS** — PTR zones have SOA records on both nodes
8. **Production untouched** — container still running, zone count unchanged
9. **Failover** — secondary serves SOA independently (no primary dependency)
10. **Credentials**`auth.config` byte-size matches across all three nodes
## Security notes
- **tailscale-router is never modified.** The only operation is `docker cp`
(read) to export the config. No writes, no restarts, no config changes.
- The temporary admin password (`TEMP_ADMIN_PW`) exists only during the
clustering step. After configuration, the production `auth.config` (with 2FA)
is restored. The temp password is never persisted.
- The export tarball (`.export/technitium-production-config.tar.gz`) contains
production credentials. It is in `.gitignore` and should be deleted after
setup: `rm -rf dns-cluster-setup/.export/`
- Each node's existing config is backed up to `config.backup-<timestamp>` before
replacement, so the change is reversible.
## Recovery
If something goes wrong, each node has a backup:
```bash
# On netinfra-01 or netinfra-02:
cd /home/localuser/services/technitium/
docker compose down
mv config config.failed
mv config.backup-<timestamp> config
docker compose up -d
```
## Validation on sandbox
After cluster setup, validate that client hosts use the pair correctly:
```bash
# From sectestbed-sandbox (or any client):
# Query primary directly:
dig @192.168.3.252 pfv-netinfra-01.knel.net
# Query secondary directly:
dig @192.168.3.253 pfv-netinfra-01.knel.net
# Both should return the same answer.
```
The KNELServerBuild provisioning code (`provisioning/ConfigFiles/NTP/ntp.conf`
and `provisioning/ConfigFiles/Resolv/resolv.conf`) points clients at both
servers for DNS and NTP redundancy. See `docs/server-build/tailscale.md` for the
full DNS architecture analysis.
@@ -35,7 +35,7 @@ mkdir -p "$ZONE_DIR"
log "Syncing zones from $PRIMARY_HOST..."
if rsync -az --delete --temp-dir=/tmp \
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
zone_count=$(ls "$ZONE_DIR" | wc -l)
log "Sync complete: $zone_count zones"
else
log "ERROR: rsync failed (rc=$?)"
@@ -69,9 +69,9 @@ echo " Production zones: $prod_zones"
echo " Primary (01) zones: $pri_zones"
echo " Secondary (02) zones: $sec_zones"
if [ "$prod_zones" -gt 0 ] 2>/dev/null; then ok "Production has $prod_zones zones"; else fail "Production zone count invalid"; fi
if [ "$pri_zones" -gt 0 ] 2>/dev/null; then ok "Primary has $pri_zones zones"; else fail "Primary zone count invalid"; fi
if [ "$sec_zones" -gt 0 ] 2>/dev/null; then ok "Secondary has $sec_zones zones"; else fail "Secondary zone count invalid"; fi
[ "$prod_zones" -gt 0 ] 2>/dev/null && ok "Production has $prod_zones zones" || fail "Production zone count invalid"
[ "$pri_zones" -gt 0 ] 2>/dev/null && ok "Primary has $pri_zones zones" || fail "Primary zone count invalid"
[ "$sec_zones" -gt 0 ] 2>/dev/null && ok "Secondary has $sec_zones zones" || fail "Secondary zone count invalid"
if [ "$pri_zones" = "$prod_zones" ]; then
ok "Primary zone count matches production ($pri_zones)"
+140
View File
@@ -0,0 +1,140 @@
<!-- Historical AI-generated review. Paths updated to current structure. -->
# AI Review: KNELServerBuild (PFVCluster) Project
## Executive Summary
The KNELServerBuild project is a comprehensive Infrastructure-as-Code (IaC) solution designed for provisioning Linux servers within the TSYS Group environment. The project implements a fetch-and-apply framework that automates the setup and hardening of server systems, incorporating security, monitoring, and operational components.
## Project Overview
The PFVCluster project is a shell-based automation framework that provisions Linux servers with:
- Security hardening (SSH, 2FA, Wazuh, STIG compliance)
- Operational monitoring (LibreNMS, cockpit, SNMP)
- System packages and configurations for enterprise operations
- Network discovery and management capabilities
## Architecture and Structure
### Key Components
- **provisioning/**: Main setup and configuration scripts
- **Project-ConfigFiles/**: Configuration variables and parameters
- **Project-Includes/**: Reusable shell functions and utilities
- **tests/**: Comprehensive testing framework
- **Modules/**: Functional modules for security, operations, etc.
- **vendor/**: External dependencies and frameworks
### Core Workflow
The `SetupNewSystem.sh` orchestrates:
1. Preflight checks and environment validation
2. Package installation and system updates
3. Service configuration and hardening
4. Security implementation (SSH, Wazuh, 2FA)
5. Operational monitoring setup
## Strengths
### 1. Comprehensive Testing Framework
- Well-structured testing with unit, integration, security, and validation categories
- Clear documentation and usage instructions
- JSON reporting for CI/CD integration
### 2. Security-First Approach
- Multiple layers of security hardening (SSH, 2FA, audit agents)
- STIG compliance for government/hybrid environments
- Proper permission management and configuration validation
### 3. Modular Architecture
- Separated concerns into functional modules
- Reusable functions and components
- Clear separation between framework and project-specific code
### 4. Operational Readiness
- Built-in monitoring and alerting
- System performance optimization
- Network discovery and management tools
### 5. Cross-Platform Considerations
- Detection for different hardware types (physical, virtual, Raspberry Pi)
- Distribution-specific handling
- Environment-aware configurations
## Areas for Improvement
### 1. Documentation Completeness
- README mentions usage but lacks detailed architecture overview
- Missing troubleshooting and recovery procedures
- Limited guidance for extending/adding new modules
### 2. Security and Secrets Management
- Configuration files may expose hardcoded credentials or tokens
- No clear secrets management strategy
- Download URLs and endpoints are hardcoded in scripts
### 3. Error Handling and Resilience
- While scripts have basic error handling, recovery mechanisms are limited
- No rollback capabilities for failed installations
- Some operations may fail silently
### 4. Scalability and Performance
- Scripts execute sequentially without parallelization
- No caching mechanisms for downloads
- Limited handling for high-latency networks
### 5. Configuration Management
- Configuration values scattered across multiple files
- No centralized configuration management
- Difficult to customize for different environments
## Recommendations
### 1. Enhance Security Practices
- Implement secrets management (HashiCorp Vault, AWS Secrets Manager, etc.)
- Add configuration validation before applying changes
- Implement digital signature verification for downloaded content
- Add security scanning of packages before installation
### 2. Improve Testing Coverage
- Add end-to-end tests for complete deployment scenarios
- Implement performance benchmarks
- Add security validation tests
- Include tests for different hardware configurations
### 3. Add Monitoring and Observability
- Implement deployment success/failure metrics
- Add progress tracking for long-running operations
- Include health checks post-deployment
- Add rollback mechanisms for failed deployments
### 4. Refactor for Maintainability
- Centralize configuration management
- Abstract environment-specific variables
- Implement plugin architecture for new modules
- Add proper logging and audit trails
### 5. Enhance Usability
- Add dry-run functionality for testing changes
- Provide rollback/recovery procedures
- Add interactive mode for new users
- Implement configuration templates
## Technical Debt Assessment
### High Priority
- Centralized configuration management
- Secrets handling and security
- Error recovery and rollback mechanisms
### Medium Priority
- Parallel execution of independent operations
- Caching for downloaded packages/configs
- Improved logging and monitoring
### Low Priority
- Code modernization (consider newer shell features)
- Migration to configuration management tools (Ansible/Terraform)
## Conclusion
The PFVCluster project represents a solid foundation for automated server provisioning with good security practices and testing. However, there are significant opportunities to improve security, maintainability, and operational resilience. Prioritizing security improvements and configuration management would provide the greatest value to the project's stability and long-term viability.
The modular architecture and comprehensive testing framework provide a strong foundation for future enhancements and improvements.
+45
View File
@@ -0,0 +1,45 @@
<!-- Historical AI-generated review. Paths updated to current structure. -->
# AI Overview of KNELServerBuild
This is an AI-generated overview of the KNELServerBuild project. The analysis is based on a read-only review of the project's files.
## Project Overview
The KNELServerBuild project is an Infrastructure as Code (IAC) repository for provisioning and configuring Linux servers. It is based on a collection of bash scripts that automate the installation of packages, configuration of services, and security hardening of the system. The project is designed to be used with the `FetchApply` tool, which is not included in this repository.
The main entry point of the project is the `provisioning/SetupNewSystem.sh` script. This script performs the following actions:
* **Initializes the environment:** Sets up project paths and sources a shell framework (`KNELShellFramework`) and project-specific includes.
* **Installs packages:** Installs a wide range of packages, including monitoring agents (check_mk, snmp), security tools (auditd, aide, lynis, clamav), administration tools (cockpit, webmin), and common utilities (tmux, vim, zsh).
* **Configures services:** Configures various services like Postfix for email, `rsyslog` for system logging, `snmpd` for monitoring, `lldpd` for network discovery, and `cockpit`.
* **Security Hardening:** It runs a series of security hardening scripts from `Modules/Security`, including `secharden-ssh.sh`, `secharden-wazuh.sh`, `secharden-2fa.sh`, and `secharden-scap-stig.sh`.
* **OAM:** It runs an OAM (Operations, Administration, and Maintenance) script for LibreNMS.
* **Conditional Logic:** It has conditional logic to apply different configurations based on whether the host is a physical Dell server, a virtual machine (KVM or Hyper-V), or a Raspberry Pi.
## What I Like
* **Well-structured:** The project is well-structured, with separate directories for code, configuration files, documentation, and tests. This makes it easy to understand and maintain.
* **Modularity:** The use of modules for different functionalities (e.g., security hardening, OAM) is a good practice. It allows for easy extension and modification of the project.
* **Comprehensive:** The project covers a wide range of aspects of server provisioning, from package installation to security hardening.
* **Conditional Logic:** The use of conditional logic to adapt the configuration to different environments is a good feature.
* **Good commenting:** The scripts are generally well-commented, which makes them easier to understand.
## Areas for Improvement
* **Error Handling:** The scripts could benefit from more robust error handling. For example, the `SetupNewSystem.sh` script uses `set -e` to exit on error, but it does not have any specific error handling logic.
* **Idempotency:** The scripts are not fully idempotent. For example, some of the `curl` commands will re-download files even if they already exist. This could be improved by adding checks to see if the files already exist.
* **Testing:** The project has a `Project-Tests` directory, but it is not clear how the tests are run or what they cover. The testing framework could be improved to provide more comprehensive coverage of the project's functionality.
* **Secrets Management:** The scripts contain some hardcoded secrets, such as the `relayhost` for Postfix. These secrets should be managed using a secrets management tool like HashiCorp Vault or AWS Secrets Manager.
* **Configuration Management:** The project uses a collection of shell scripts to manage the configuration of the system. While this works, it can be difficult to manage and maintain in the long run. A configuration management tool like Ansible, Puppet, or Chef would be a better choice for this task. The project already installs `ansible-core`, so it would be a natural progression to move the logic to Ansible playbooks.
* **Documentation:** The project has some documentation, but it could be improved. For example, the `README.md` file could provide more information on how to use the project and how to contribute to it.
## Recommendations
* **Improve Error Handling:** Add more robust error handling to the scripts to make them more reliable.
* **Improve Idempotency:** Make the scripts more idempotent to avoid unnecessary re-downloads and re-configurations.
* **Improve Testing:** Implement a more comprehensive testing framework to ensure the quality of the project.
* **Use a Secrets Management Tool:** Use a secrets management tool to manage the secrets in the project.
* **Use a Configuration Management Tool:** Use a configuration management tool like Ansible to manage the configuration of the system.
* **Improve Documentation:** Improve the documentation of the project to make it easier to use and contribute to.
Overall, the KNELServerBuild project is a good starting point for an IAC repository. It is well-structured and covers a wide range of aspects of server provisioning. However, there are some areas where it could be improved. By addressing the areas for improvement, the project can be made more robust, reliable, and maintainable.
+309
View File
@@ -0,0 +1,309 @@
<!-- Historical AI-generated review. Paths updated to current structure. -->
# AI Overview: KNEL Server Build (FetchApply) Project
**Date:** December 26, 2025
**Reviewer:** OpenCode AI Assistant
**Project:** TSYS Infrastructure Provisioning System
## Executive Summary
The KNEL Server Build project is a comprehensive Infrastructure as Code (IaC) system for Linux server provisioning and security hardening. It demonstrates strong architectural patterns with a modular framework approach but has several areas requiring improvement for production readiness, security, and maintainability.
## Architecture Assessment
### Strengths ✅
**1. Modular Framework Design**
- Well-structured KNELShellFramework with centralized includes
- Clear separation between framework, project code, and configuration
- Consistent pattern for sourcing framework components
- Proper abstraction of common functionality
**2. Comprehensive Security Modules**
- Extensive security hardening capabilities (SSH, Wazuh, 2FA, SCAP/STIG)
- HTTPS enforcement throughout
- Proper audit logging integration
- Good compliance focus with industry standards
**3. Testing Infrastructure**
- Automated test suite with multiple categories (unit, integration, security, validation)
- JSON-based test reporting
- Good test organization and coverage
**4. Documentation Excellence**
- Comprehensive deployment guide with troubleshooting
- Detailed development guidelines with best practices
- Security documentation with threat model
- Code review findings and refactoring examples
### Areas for Improvement ⚠️
**1. Performance Issues**
- Multiple separate package installation commands instead of consolidated approach
- Individual file downloads causing network overhead
- No connection pooling for multiple downloads from same host
**2. Security Vulnerabilities**
- SSH keys stored in git repository (secrets management needed)
- No download integrity verification (checksum validation)
- Missing comprehensive input validation
- Unquoted variable expansions creating injection risks
**3. Error Handling Gaps**
- Network operations lack timeout and retry logic
- Inconsistent error handling across modules
- Missing graceful failure handling in critical paths
## Technical Debt Analysis
### High Priority Issues
**1. Package Installation Performance**
```bash
# Current inefficient pattern in SetupNewSystem.sh
apt-get -y install git sudo dmidecode curl # Line 27
# Later: separate massive apt-get command
```
**Impact:** 30-40% slower deployments, multiple package cache updates
**2. Network Resilience**
```bash
# Vulnerable pattern throughout codebase
curl --silent ${DL_ROOT}/path/file >/etc/config
```
**Impact:** Deployment failures in poor network conditions, no recovery mechanism
**3. Variable Quoting Security**
```bash
# Risky pattern
chsh -s $(which zsh) root
```
**Impact:** Potential command injection vulnerabilities
### Medium Priority Issues
**1. Framework Consistency**
- Not all modules follow established error handling patterns
- Inconsistent logging and progress reporting
- Mixed coding standards across different components
**2. Testing Coverage**
- Limited integration testing for complex workflows
- Missing performance benchmarking tests
- No automated regression testing for configuration changes
## Recommendations
### Immediate Actions (Week 1-2)
**1. Implement Safe Download Framework**
```bash
# Create centralized download function with:
# - Connection timeouts (30s)
# - Retry logic (3 attempts)
# - Checksum validation
# - Error recovery
```
**2. Consolidate Package Management**
```bash
# Single package installation with logical grouping:
# - Core system tools
# - Security packages
# - Monitoring tools
# - Development utilities
```
**3. Fix Variable Quoting**
- Audit entire codebase for unquoted variables
- Implement static analysis check in CI pipeline
- Add input validation framework
### Medium-term Improvements (Month 1-2)
**1. Secrets Management**
- Remove SSH keys from repository
- Integrate Bitwarden/Vault for secret storage
- Implement key rotation procedures
**2. Performance Optimization**
- Implement batch download operations
- Add connection pooling
- Create deployment metrics collection
**3. Enhanced Testing**
- Add performance benchmarking
- Implement chaos engineering for network failures
- Create automated regression testing
### Long-term Enhancements (Quarter 1)
**1. Infrastructure Improvements**
- Implement configuration backup/restore
- Add rollback capability for failed deployments
- Create deployment pipeline with staging environments
**2. Advanced Security**
- Implement supply chain security with SBOM
- Add automated vulnerability scanning
- Create security compliance reporting
## Code Quality Assessment
### Positive Patterns
- Good function documentation in recent code
- Proper error handling in newer modules
- Consistent use of framework logging functions
- Clear separation of concerns
### Problem Patterns
- Mixed coding styles across files
- Inconsistent framework usage
- Missing input validation
- Hardcoded configuration values
### Modernization Opportunities
**1. Containerization**
- Consider Docker-based deployment testing
- Create immutable infrastructure patterns
- Implement blue-green deployments
**2. Configuration Management**
- Move to declarative configuration approach
- Implement configuration drift detection
- Add automated compliance checking
**3. Observability**
- Implement comprehensive logging with structured formats
- Add metrics collection for deployment performance
- Create dashboard for system health monitoring
## Security Posture Review
### Current Strengths
- HTTPS-only downloads
- Good SSH hardening practices
- Comprehensive audit logging
- Regular security scanning integration
### Critical Gaps
- No integrity verification for downloads
- Secrets stored in version control
- Limited defense in depth
- Missing automated security testing
### Recommended Security Enhancements
**1. Supply Chain Security**
- Implement checksum validation for all downloads
- Add GPG signature verification where available
- Create SBOM generation for deployments
**2. Access Control**
- Implement role-based access control
- Add privileged access management
- Create audit trail for all administrative actions
**3. Continuous Security**
- Integrate automated vulnerability scanning
- Implement security testing in CI/CD
- Create security metrics dashboard
## Deployment Readiness Assessment
### Current State: **70% Production Ready**
**Ready Components:**
- Core provisioning functionality
- Security hardening modules
- Basic testing framework
- Documentation
**Missing Components:**
- Robust error handling
- Performance optimization
- Secrets management
- Comprehensive testing
### Path to Production Readiness
**Phase 1 (2 weeks):** Critical fixes and performance optimization
**Phase 2 (4 weeks):** Security enhancements and testing improvements
**Phase 3 (8 weeks):** Advanced features and production hardening
## Overall Assessment
### What I Like 🎯
**1. Architectural Excellence**
- The KNELShellFramework shows mature thinking about code organization
- Modular approach allows for easy maintenance and extension
- Clear separation of concerns between framework and project code
**2. Security-First Mindset**
- Comprehensive security hardening capabilities
- Good threat awareness and mitigation strategies
- Integration with industry-standard security tools
**3. Documentation Quality**
- Excellent documentation with practical examples
- Clear deployment guides with troubleshooting sections
- Good development guidelines for team consistency
### What I Don't Like 🚫
**1. Performance Oversights**
- Multiple package installations causing unnecessary delays
- Individual file downloads creating network overhead
- No performance metrics or monitoring
**2. Security Gaps**
- Critical vulnerability with secrets in git repository
- No download integrity verification
- Missing comprehensive input validation
**3. Code Quality Issues**
- Inconsistent error handling across modules
- Variable quoting creating security risks
- Mixed coding standards throughout codebase
### Improvement Potential 📈
**1. Immediate Impact (High ROI)**
- Package installation consolidation: 30-40% performance improvement
- Safe download framework: 90% reduction in network-related failures
- Variable quoting fixes: Eliminate security vulnerabilities
**2. Medium-term Benefits**
- Secrets management: Eliminate critical security risks
- Performance optimization: Better user experience
- Enhanced testing: Higher reliability and confidence
**3. Long-term Value**
- Containerization: Modern deployment patterns
- Observability: Better operational insight
- Automation: Reduced manual overhead
## Final Recommendation
The KNEL Server Build project demonstrates solid architectural foundations and comprehensive security capabilities. With focused improvements in performance optimization, security hardening (particularly secrets management), and error handling, this system can become a production-grade infrastructure provisioning solution.
**Priority:**
1. **Immediate:** Fix security vulnerabilities and performance bottlenecks
2. **Short-term:** Enhance testing and error handling
3. **Long-term:** Implement advanced features and modernization
**Investment Justification:** The project shows strong potential with a clear path to production readiness. The modular architecture and comprehensive security focus make it a valuable foundation for enterprise infrastructure automation.
---
**Next Steps:**
1. Create implementation roadmap for critical fixes
2. Establish performance benchmarks
3. Implement continuous integration with quality gates
4. Plan phased rollout to production environments
**Risk Level:** Medium - manageable with proper remediation plan
**Business Value:** High - significant time savings and security improvements
**Technical Debt:** Moderate - requires systematic but achievable refactoring
+29
View File
@@ -0,0 +1,29 @@
<!-- Historical AI-generated security review. Paths updated where actionable. -->
<!-- Historical AI-generated review. Paths updated to current structure. -->
# AI Security Audit of KNELServerBuild
This is an AI-generated security audit of the KNELServerBuild project. The analysis is based on a read-only review of the project's files.
## Summary of Findings
The KNELServerBuild project has a good security posture overall, but there are a few areas that could be improved. The most significant finding is the presence of SSH authorized keys in the repository. This is a security risk, as it allows anyone with access to the repository to know which public keys are authorized to access the servers.
### High-Risk Findings
* **SSH Authorized Keys in Repository:** The `provisioning/ConfigFiles/SSH/AuthorizedKeys` directory contains SSH authorized keys for the `localuser` and `root` users. This is a security risk, as it allows anyone with access to the repository to know which public keys are authorized to access the servers.
### Medium-Risk Findings
* **Hardcoded Hostnames:** The scripts contain several hardcoded hostnames for services like Postfix, NTP, syslog, and Wazuh. This is not a direct security risk, but it does represent a configuration management issue. If any of these hostnames change, they will need to be updated in multiple places.
### Low-Risk Findings
* **Potential for Password on Command Line:** The `provisioning/Agents/librenms/mysql.sh` script has a `--pass` argument for a MySQL password. This is a potential security risk if the password is provided on the command line, as it could be logged in the shell history.
## Recommendations
* **Remove SSH Authorized Keys from Repository:** The SSH authorized keys should be removed from the repository and managed using a secrets management tool like HashiCorp Vault or AWS Secrets Manager.
* **Use Variables for Hostnames:** The hardcoded hostnames should be replaced with variables that are defined in a central configuration file. This will make it easier to update the hostnames if they change.
* **Avoid Passwords on Command Line:** The `provisioning/Agents/librenms/mysql.sh` script should be modified to avoid passing the MySQL password on the command line. For example, the script could prompt the user for the password or read it from a configuration file.
Overall, the KNELServerBuild project is a good starting point for an IAC repository. By addressing the security risks identified in this audit, the project can be made more secure and reliable.
+280
View File
@@ -0,0 +1,280 @@
<!-- Historical AI-generated review. Paths may reference pre-merge structure. -->
# TSYS PFVCluster Code Review Findings
**Review Date:** July 14, 2025
**Reviewer:** Claude (Anthropic)
**Repository:** TSYS Group Infrastructure Provisioning Scripts
## Executive Summary
The repository shows good architectural structure with centralized framework components, but has several performance, security, and maintainability issues that require attention. The codebase is functional but needs optimization for production reliability.
## Critical Issues (High Priority)
### 1. Package Installation Performance ⚠️
**Location:** `provisioning/SetupNewSystem.sh:27` and `Lines 117-183`
**Issue:** Multiple separate package installation commands causing performance bottlenecks
```bash
# Current inefficient pattern
apt-get -y install git sudo dmidecode curl
# ... later in script ...
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes install virt-what auditd ...
```
**Impact:** Significantly slower deployment, multiple package cache updates
**Fix:** Combine all package installations into single command
### 2. Network Operations Lack Error Handling 🔴
**Location:** `provisioning/SetupNewSystem.sh:61-63`, multiple modules
**Issue:** curl commands without timeout or error handling
```bash
# Vulnerable pattern
curl --silent ${DL_ROOT}/path/file >/etc/config
```
**Impact:** Deployment failures in poor network conditions
**Fix:** Add timeout, error handling, and retry logic
### 3. Unquoted Variable Expansions 🔴
**Location:** Multiple files, including `provisioning/SetupNewSystem.sh:244`
**Issue:** Variables used without proper quoting creating security risks
```bash
# Risky pattern
chsh -s $(which zsh) root
```
**Impact:** Potential command injection, script failures
**Fix:** Quote all variable expansions consistently
## Security Concerns
### 4. No Download Integrity Verification 🔴
**Issue:** All remote downloads lack checksum verification
**Impact:** Supply chain attack vulnerability
**Recommendation:** Implement SHA256 checksum validation
### 5. Excessive Root Privilege Usage ⚠️
**Issue:** All operations run as root without privilege separation
**Impact:** Unnecessary security exposure
**Recommendation:** Delegate non-privileged operations when possible
## Performance Optimization Opportunities
### 6. Individual File Downloads 🟡
**Location:** `provisioning/Modules/Security/secharden-scap-stig.sh:66-77`
**Issue:** 12+ individual curl commands for config files
```bash
curl --silent ${DL_ROOT}/path1 > /etc/file1
curl --silent ${DL_ROOT}/path2 > /etc/file2
# ... repeated 12+ times
```
**Impact:** Network overhead, slower deployment
**Fix:** Batch download operations
### 7. Missing Connection Pooling ⚠️
**Issue:** No connection reuse for multiple downloads from same host
**Impact:** Unnecessary connection overhead
**Fix:** Use curl with connection reuse or wget with keep-alive
## Code Quality Issues
### 8. Inconsistent Framework Usage 🟡
**Issue:** Not all modules use established error handling framework
**Impact:** Inconsistent error reporting, debugging difficulties
**Fix:** Standardize framework usage across all modules
### 9. Incomplete Function Implementations 🟡
**Location:** `Framework-Includes/LookupKv.sh`
**Issue:** Stubbed functions with no implementation
**Impact:** Technical debt, confusion
**Fix:** Implement or remove unused functions
### 10. Missing Input Validation 🟡
**Location:** `Project-Includes/pi-detect.sh`
**Issue:** Functions lack proper input validation and quoting
**Impact:** Potential script failures
**Fix:** Add comprehensive input validation
## Recommended Immediate Actions
### Phase 1: Critical Fixes (Week 1)
1. **Fix variable quoting** throughout codebase
2. **Add error handling** to all network operations
3. **Combine package installations** for performance
4. **Implement download integrity verification**
### Phase 2: Performance Optimization (Week 2)
1. **Batch file download operations**
2. **Add connection timeouts and retries**
3. **Implement bulk configuration deployment**
4. **Optimize service restart procedures**
### Phase 3: Code Quality (Week 3-4)
1. **Standardize framework usage**
2. **Add comprehensive input validation**
3. **Implement proper logging with timestamps**
4. **Remove or complete stubbed functions**
## Specific Code Improvements
### Enhanced Error Handling Pattern
```bash
function safe_download() {
local url="$1"
local dest="$2"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if curl --silent --connect-timeout 30 --max-time 60 --fail "$url" > "$dest"; then
print_success "Downloaded: $(basename "$dest")"
return 0
else
print_warning "Download attempt $attempt failed: $url"
((attempt++))
sleep 5
fi
done
print_error "Failed to download after $max_attempts attempts: $url"
return 1
}
```
### Bulk Package Installation Pattern
```bash
function install_all_packages() {
print_info "Installing all required packages..."
local packages=(
# Core system packages
git sudo dmidecode curl wget
# Security packages
auditd fail2ban aide
# Monitoring packages
snmpd snmp-mibs-downloader
# Additional packages
virt-what net-tools htop
)
if DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install "${packages[@]}"; then
print_success "All packages installed successfully"
else
print_error "Package installation failed"
return 1
fi
}
```
### Batch Configuration Download
```bash
function download_configurations() {
print_info "Downloading configuration files..."
local -A configs=(
["${DL_ROOT}/provisioning/ConfigFiles/ZSH/tsys-zshrc"]="/etc/zshrc"
["${DL_ROOT}/provisioning/ConfigFiles/SMTP/aliases"]="/etc/aliases"
["${DL_ROOT}/provisioning/ConfigFiles/Syslog/rsyslog.conf"]="/etc/rsyslog.conf"
)
for url in "${!configs[@]}"; do
local dest="${configs[$url]}"
if ! safe_download "$url" "$dest"; then
return 1
fi
done
print_success "All configurations downloaded"
}
```
## Testing Recommendations
### Add Performance Tests
```bash
function test_package_installation_performance() {
local start_time=$(date +%s)
install_all_packages
local end_time=$(date +%s)
local duration=$((end_time - start_time))
echo "✅ Package installation completed in ${duration}s"
if [[ $duration -gt 300 ]]; then
echo "⚠️ Installation took longer than expected (>5 minutes)"
fi
}
```
### Add Network Resilience Tests
```bash
function test_network_error_handling() {
# Test with invalid URL
if safe_download "https://invalid.example.com/file" "/tmp/test"; then
echo "❌ Error handling test failed - should have failed"
return 1
else
echo "✅ Error handling test passed"
return 0
fi
}
```
## Monitoring and Metrics
### Deployment Performance Metrics
- **Package installation time:** Should complete in <5 minutes
- **Configuration download time:** Should complete in <2 minutes
- **Service restart time:** Should complete in <30 seconds
- **Total deployment time:** Should complete in <15 minutes
### Error Rate Monitoring
- **Network operation failures:** Should be <1%
- **Package installation failures:** Should be <0.1%
- **Service restart failures:** Should be <0.1%
## Compliance Assessment
### Development Guidelines Adherence
**Good:** Single package commands in newer modules
**Good:** Framework integration patterns
**Good:** Function documentation in recent code
**Needs Work:** Variable quoting consistency
**Needs Work:** Error handling standardization
**Needs Work:** Input validation coverage
## Risk Assessment
**Current Risk Level:** Medium
**Key Risks:**
1. **Deployment failures** due to network issues
2. **Security vulnerabilities** from unvalidated downloads
3. **Performance issues** in production deployments
4. **Maintenance challenges** from code inconsistencies
**Mitigation Priority:**
1. Network error handling (High)
2. Download integrity verification (High)
3. Performance optimization (Medium)
4. Code standardization (Medium)
## Conclusion
The TSYS PFVCluster repository has a solid foundation but requires systematic improvements to meet production reliability standards. The recommended fixes will significantly enhance:
- **Deployment reliability** through better error handling
- **Security posture** through integrity verification
- **Performance** through optimized operations
- **Maintainability** through code standardization
Implementing these improvements in the suggested phases will create a robust, production-ready infrastructure provisioning system.
---
**Next Steps:**
1. Review and prioritize findings with development team
2. Create implementation plan for critical fixes
3. Establish testing procedures for improvements
4. Set up monitoring for deployment metrics
+94
View File
@@ -0,0 +1,94 @@
<!-- Historical AI-generated review. Paths updated to current structure. -->
# Claude Code Review - TSYS PFVCluster Infrastructure
**Review Date:** July 14, 2025 (Updated)
**Reviewed by:** Claude (Anthropic)
**Repository:** TSYS Group Infrastructure Provisioning Scripts
**Previous Review:** July 12, 2025
## Project Overview
This repository contains infrastructure-as-code for provisioning Linux servers in the TSYS Group environment. The codebase includes 32 shell scripts (~2,800 lines) organized into a modular framework for system hardening, security configuration, and operational tooling deployment.
## Strengths ✅
### Security Hardening
- **SSH Security:** Comprehensive SSH hardening with key-only authentication, disabled password login, and secure cipher configurations
- **Security Agents:** Automated deployment of Wazuh SIEM agents, audit tools, and SCAP-STIG compliance checking
- **File Permissions:** Proper restrictive permissions (400 for SSH keys, 644 for configs)
- **Network Security:** Firewall configuration, network discovery tools (LLDP), and monitoring agents
### Code Quality
- **Error Handling:** Robust bash strict mode implementation (`set -euo pipefail`) with custom error trapping and line number reporting
- **Modular Design:** Well-organized structure separating framework components, configuration files, and functional modules
- **Environment Awareness:** Intelligent detection of physical vs virtual hosts, distribution-specific logic, and hardware-specific optimizations
- **Logging:** Centralized logging with timestamp-based log files and colored output for debugging
### Operational Excellence
- **Package Management:** Automated repository setup for security tools (Lynis, Webmin, Tailscale, Wazuh)
- **System Tuning:** Performance optimizations for physical hosts, virtualization-aware configurations
- **Monitoring Integration:** LibreNMS agents, SNMP configuration, and system metrics collection
## Security Concerns ⚠️
### Critical Issues
1. **~~Insecure Deployment Method~~** ✅ **RESOLVED:** Now uses `git clone` + local script execution instead of `curl | bash`
2. **No Integrity Verification:** Downloaded scripts lack checksum validation or cryptographic signatures
3. **~~HTTP Downloads~~** ✅ **RESOLVED:** All HTTP URLs converted to HTTPS (Dell OMSA, Proxmox, Apache sources)
### Moderate Risks
4. **Exposed SSH Keys:** Public SSH keys committed directly to repository without rotation mechanism
5. **Hard-coded Credentials:** Server hostnames and domain names embedded in scripts
6. **Missing Secrets Management:** No current implementation of Bitwarden/Vault integration (noted in TODO comments)
## Improvement Recommendations 🔧
### High Priority (Security Critical)
1. **~~Secure Deployment Pipeline~~** ✅ **RESOLVED:** Now uses git clone-based deployment
2. **~~HTTPS Enforcement~~** ✅ **RESOLVED:** All HTTP downloads converted to HTTPS
3. **Script Integrity:** Implement SHA256 checksum verification for all downloaded components
4. **Secrets Management:** Deploy proper secrets handling for SSH keys and sensitive configurations
### Medium Priority (Operational)
5. **Testing Framework:** Add integration tests for provisioning workflows
6. **Documentation Enhancement:** Expand security considerations and deployment procedures
7. **Configuration Validation:** Add pre-deployment validation of system requirements
8. **Rollback Capability:** Implement configuration backup and rollback mechanisms
### Low Priority (Quality of Life)
9. **Error Recovery:** Enhanced error recovery and partial deployment resumption
10. **Monitoring Integration:** Centralized logging and deployment status reporting
11. **User Interface:** Consider web-based deployment dashboard for non-technical users
## Risk Assessment 📊
**Overall Risk Level:** Low-Medium ⬇️ (Reduced from Medium-Low)
The repository contains well-architected defensive security tools with strong error handling and modular design. **Major security improvement:** The insecure `curl | bash` deployment method has been replaced with git-based deployment. Remaining concerns are primarily around hardening the provisioning scripts themselves rather than the deployment method.
**Recommendation:** Continue addressing remaining security issues (HTTPS enforcement, secrets management) but the critical deployment risk has been mitigated. The codebase is much safer for production use.
## Update Summary (July 14, 2025)
**✅ Resolved Issues:**
- Insecure deployment method replaced with git clone approach
- README.md updated with project management and community links
- Deployment security risk significantly reduced
- All HTTP URLs converted to HTTPS (Dell OMSA, Proxmox, Apache sources)
**🔄 Remaining Priorities:**
1. ~~HTTPS enforcement for internal downloads~~**RESOLVED:** All HTTP URLs converted to HTTPS
2. Secrets management implementation
3. Script integrity verification
4. SSH key rotation from repository
## Files Reviewed
- 32 shell scripts across Framework-Includes, Project-Includes, and ProjectCode directories
- Configuration files for SSH, SNMP, logging, and system services
- Security modules for hardening, authentication, and monitoring
- Documentation and framework configuration files
## Next Steps
See `charles-todo.md` and `claude-todo.md` for detailed action items prioritized for human operators and AI assistants respectively.
+535
View File
@@ -0,0 +1,535 @@
<!-- Historical document: paths and patterns shown are pre-refactor. See provisioning/ for current code. -->
# Code Refactoring Examples
This document provides specific examples of how to apply the code review findings to improve performance, security, and reliability.
## Package Installation Optimization
### Before (Current - Multiple Commands)
```bash
# Line 27 in SetupNewSystem.sh
apt-get -y install git sudo dmidecode curl
# Lines 117-183 (later in script)
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
virt-what \
auditd \
aide \
# ... many more packages
```
### After (Optimized - Single Command)
```bash
function install_all_packages() {
print_info "Installing all required packages..."
# All packages in logical groups for better readability
local packages=(
# Core system tools
git sudo dmidecode curl wget net-tools htop
# Security and auditing
auditd aide fail2ban lynis rkhunter
# Monitoring and SNMP
snmpd snmp-mibs-downloader libsnmp-dev
# Virtualization detection
virt-what
# System utilities
rsyslog logrotate ntp ntpdate
cockpit cockpit-ws cockpit-system
# Development and debugging
build-essential dkms
# Network services
openssh-server ufw
)
# Single package installation command with retry logic
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install "${packages[@]}"; then
print_success "All packages installed successfully"
return 0
else
print_warning "Package installation attempt $attempt failed"
if [[ $attempt -lt $max_attempts ]]; then
print_info "Retrying in 10 seconds..."
sleep 10
apt-get update # Refresh package cache before retry
fi
((attempt++))
fi
done
print_error "Package installation failed after $max_attempts attempts"
return 1
}
```
## Safe Download Implementation
### Before (Current - Unsafe Downloads)
```bash
# Lines 61-63 in SetupNewSystem.sh
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc >/etc/zshrc
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases >/etc/aliases
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf >/etc/rsyslog.conf
```
### After (Safe Downloads with Error Handling)
```bash
function download_system_configs() {
print_info "Downloading system configuration files..."
# Source the safe download framework
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
# Define configuration downloads with checksums (optional)
declare -A config_downloads=(
["${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc"]="/etc/zshrc"
["${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases"]="/etc/aliases"
["${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf"]="/etc/rsyslog.conf"
["${DL_ROOT}/ProjectCode/ConfigFiles/SSH/Configs/tsys-sshd-config"]="/etc/ssh/sshd_config.tsys"
)
# Validate all URLs are accessible before starting
local urls=()
for url in "${!config_downloads[@]}"; do
urls+=("$url")
done
if ! validate_required_urls "${urls[@]}"; then
print_error "Some configuration URLs are not accessible"
return 1
fi
# Perform batch download with backup
local failed_downloads=0
for url in "${!config_downloads[@]}"; do
local dest="${config_downloads[$url]}"
if ! safe_config_download "$url" "$dest"; then
((failed_downloads++))
fi
done
if [[ $failed_downloads -eq 0 ]]; then
print_success "All configuration files downloaded successfully"
return 0
else
print_error "$failed_downloads configuration downloads failed"
return 1
fi
}
```
## Variable Quoting Fixes
### Before (Unsafe Variable Usage)
```bash
# Line 244 in SetupNewSystem.sh
chsh -s $(which zsh) root
# Multiple instances throughout codebase
if [ -f $CONFIG_FILE ]; then
cp $CONFIG_FILE $BACKUP_DIR
fi
```
### After (Proper Variable Quoting)
```bash
# Safe variable usage with proper quoting
chsh -s "$(which zsh)" root
# Consistent quoting pattern
if [[ -f "$CONFIG_FILE" ]]; then
cp "$CONFIG_FILE" "$BACKUP_DIR/"
fi
# Function parameter handling
function configure_service() {
local service_name="$1"
local config_file="$2"
if [[ -z "$service_name" || -z "$config_file" ]]; then
print_error "configure_service: service name and config file required"
return 1
fi
print_info "Configuring service: $service_name"
# Safe operations with quoted variables
}
```
## Service Management with Error Handling
### Before (Basic Service Operations)
```bash
# Current pattern in various modules
systemctl restart snmpd
systemctl enable snmpd
```
### After (Robust Service Management)
```bash
function safe_service_restart() {
local service="$1"
local config_test_cmd="${2:-}"
if [[ -z "$service" ]]; then
print_error "safe_service_restart: service name required"
return 1
fi
print_info "Managing service: $service"
# Test configuration if test command provided
if [[ -n "$config_test_cmd" ]]; then
print_info "Testing $service configuration..."
if ! eval "$config_test_cmd"; then
print_error "$service configuration test failed"
return 1
fi
print_success "$service configuration test passed"
fi
# Check if service exists
if ! systemctl list-unit-files "$service.service" >/dev/null 2>&1; then
print_error "Service $service does not exist"
return 1
fi
# Stop service if running
if systemctl is-active "$service" >/dev/null 2>&1; then
print_info "Stopping $service..."
if ! systemctl stop "$service"; then
print_error "Failed to stop $service"
return 1
fi
fi
# Start and enable service
print_info "Starting and enabling $service..."
if systemctl start "$service" && systemctl enable "$service"; then
print_success "$service started and enabled successfully"
# Verify service is running
sleep 2
if systemctl is-active "$service" >/dev/null 2>&1; then
print_success "$service is running properly"
return 0
else
print_error "$service failed to start properly"
return 1
fi
else
print_error "Failed to start or enable $service"
return 1
fi
}
# Usage examples
safe_service_restart "sshd" "sshd -t"
safe_service_restart "snmpd"
safe_service_restart "rsyslog"
```
## Batch Configuration Deployment
### Before (Individual File Operations)
```bash
# Lines 66-77 in secharden-scap-stig.sh
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/usb_storage.conf > /etc/modprobe.d/usb_storage.conf
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/dccp.conf > /etc/modprobe.d/dccp.conf
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/rds.conf > /etc/modprobe.d/rds.conf
# ... 12 more individual downloads
```
### After (Batch Operations with Error Handling)
```bash
function deploy_modprobe_configs() {
print_info "Deploying modprobe security configurations..."
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
local modprobe_configs=(
"usb_storage" "dccp" "rds" "sctp" "tipc"
"cramfs" "freevxfs" "hfs" "hfsplus"
"jffs2" "squashfs" "udf"
)
# Create download map
declare -A config_downloads=()
for config in "${modprobe_configs[@]}"; do
local url="${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/${config}.conf"
local dest="/etc/modprobe.d/${config}.conf"
config_downloads["$url"]="$dest"
done
# Validate URLs first
local urls=()
for url in "${!config_downloads[@]}"; do
urls+=("$url")
done
if ! validate_required_urls "${urls[@]}"; then
print_error "Some modprobe configuration URLs are not accessible"
return 1
fi
# Perform batch download
if batch_download config_downloads; then
print_success "All modprobe configurations deployed"
# Update initramfs to apply changes
if update-initramfs -u; then
print_success "Initramfs updated with new module configurations"
else
print_warning "Failed to update initramfs - reboot may be required"
fi
return 0
else
print_error "Failed to deploy some modprobe configurations"
return 1
fi
}
```
## Input Validation and Error Handling
### Before (Minimal Validation)
```bash
# pi-detect.sh current implementation
function pi-detect() {
print_info Now running "$FUNCNAME"....
if [ -f /sys/firmware/devicetree/base/model ] ; then
export IS_RASPI="1"
fi
}
```
### After (Comprehensive Validation)
```bash
function pi-detect() {
print_info "Now running $FUNCNAME..."
# Initialize variables with default values
export IS_RASPI="0"
export PI_MODEL=""
export PI_REVISION=""
# Check for Raspberry Pi detection file
local device_tree_model="/sys/firmware/devicetree/base/model"
local cpuinfo_file="/proc/cpuinfo"
if [[ -f "$device_tree_model" ]]; then
# Try device tree method first (most reliable)
local model_info
model_info=$(tr -d '\0' < "$device_tree_model" 2>/dev/null)
if [[ "$model_info" =~ [Rr]aspberry.*[Pp]i ]]; then
export IS_RASPI="1"
export PI_MODEL="$model_info"
print_success "Raspberry Pi detected via device tree: $PI_MODEL"
fi
elif [[ -f "$cpuinfo_file" ]]; then
# Fallback to cpuinfo method
if grep -qi "raspberry" "$cpuinfo_file"; then
export IS_RASPI="1"
PI_MODEL=$(grep "^Model" "$cpuinfo_file" | cut -d: -f2 | sed 's/^[[:space:]]*//' 2>/dev/null || echo "Unknown Pi Model")
PI_REVISION=$(grep "^Revision" "$cpuinfo_file" | cut -d: -f2 | sed 's/^[[:space:]]*//' 2>/dev/null || echo "Unknown")
export PI_MODEL
export PI_REVISION
print_success "Raspberry Pi detected via cpuinfo: $PI_MODEL (Rev: $PI_REVISION)"
fi
fi
if [[ "$IS_RASPI" == "1" ]]; then
print_info "Raspberry Pi specific optimizations will be applied"
else
print_info "Standard x86/x64 system detected"
fi
return 0
}
```
## Function Framework Integration
### Before (Inconsistent Framework Usage)
```bash
# Mixed patterns throughout codebase
function some_function() {
echo "Doing something..."
command_that_might_fail
echo "Done"
}
```
### After (Standardized Framework Integration)
```bash
function some_function() {
print_info "Now running $FUNCNAME..."
# Local variables
local config_file="/etc/example.conf"
local backup_dir="/root/backup"
local failed=0
# Validate prerequisites
if [[ ! -d "$backup_dir" ]]; then
if ! mkdir -p "$backup_dir"; then
print_error "Failed to create backup directory: $backup_dir"
return 1
fi
fi
# Backup existing configuration
if [[ -f "$config_file" ]]; then
if cp "$config_file" "$backup_dir/$(basename "$config_file").bak.$(date +%Y%m%d-%H%M%S)"; then
print_info "Backed up existing configuration"
else
print_error "Failed to backup existing configuration"
return 1
fi
fi
# Perform main operation with error handling
if command_that_might_fail; then
print_success "Operation completed successfully"
else
print_error "Operation failed"
return 1
fi
print_success "Completed $FUNCNAME"
return 0
}
```
## Performance Monitoring Integration
### Enhanced Deployment with Metrics
```bash
function deploy_with_metrics() {
local start_time end_time duration
local operation_name="$1"
shift
local operation_function="$1"
shift
print_info "Starting $operation_name..."
start_time=$(date +%s)
# Execute the operation
if "$operation_function" "$@"; then
end_time=$(date +%s)
duration=$((end_time - start_time))
print_success "$operation_name completed in ${duration}s"
# Log performance metrics
echo "$(date '+%Y-%m-%d %H:%M:%S') - $operation_name: ${duration}s" >> /var/log/fetchapply-performance.log
# Alert if operation took too long
case "$operation_name" in
"Package Installation")
if [[ $duration -gt 300 ]]; then
print_warning "Package installation took longer than expected (${duration}s > 300s)"
fi
;;
"Configuration Download")
if [[ $duration -gt 120 ]]; then
print_warning "Configuration download took longer than expected (${duration}s > 120s)"
fi
;;
esac
return 0
else
end_time=$(date +%s)
duration=$((end_time - start_time))
print_error "$operation_name failed after ${duration}s"
echo "$(date '+%Y-%m-%d %H:%M:%S') - $operation_name: FAILED after ${duration}s" >> /var/log/fetchapply-performance.log
return 1
fi
}
# Usage example
deploy_with_metrics "Package Installation" install_all_packages
deploy_with_metrics "Configuration Download" download_system_configs
deploy_with_metrics "SSH Hardening" configure_ssh_hardening
```
## Testing Integration
### Comprehensive Validation Function
```bash
function validate_deployment() {
print_header "Deployment Validation"
local validation_failures=0
# Test package installation
local required_packages=("git" "curl" "wget" "snmpd" "auditd" "fail2ban")
for package in "${required_packages[@]}"; do
if dpkg -l | grep -q "^ii.*$package"; then
print_success "Package installed: $package"
else
print_error "Package missing: $package"
((validation_failures++))
fi
done
# Test service status
local required_services=("sshd" "snmpd" "auditd" "rsyslog")
for service in "${required_services[@]}"; do
if systemctl is-active "$service" >/dev/null 2>&1; then
print_success "Service running: $service"
else
print_error "Service not running: $service"
((validation_failures++))
fi
done
# Test configuration files
local required_configs=("/etc/ssh/sshd_config" "/etc/snmp/snmpd.conf" "/etc/rsyslog.conf")
for config in "${required_configs[@]}"; do
if [[ -f "$config" && -s "$config" ]]; then
print_success "Configuration exists: $(basename "$config")"
else
print_error "Configuration missing or empty: $(basename "$config")"
((validation_failures++))
fi
done
# Run security tests
if command -v lynis >/dev/null 2>&1; then
print_info "Running basic security audit..."
if lynis audit system --quick --quiet; then
print_success "Security audit completed"
else
print_warning "Security audit found issues"
fi
fi
# Summary
if [[ $validation_failures -eq 0 ]]; then
print_success "All deployment validation checks passed"
return 0
else
print_error "$validation_failures deployment validation checks failed"
return 1
fi
}
```
These refactoring examples demonstrate how to apply the code review findings to create more robust, performant, and maintainable infrastructure provisioning scripts.
+117
View File
@@ -0,0 +1,117 @@
# Charles TODO - PFVCluster Security Improvements
**Priority Order:** High → Medium → Low
**Target:** Address security vulnerabilities and operational improvements
## 🚨 HIGH PRIORITY (Security Critical)
### ✅ 1. Replace Insecure Deployment Method - RESOLVED
**Previous Issue:** `curl https://dl.knownelement.com/KNEL/FetchApply/SetupNewSystem.sh | bash`
**Status:** Fixed in README.md - now uses secure git clone approach
**Current Method:** `git clone this repo``cd PFVCluster/provisioning``bash SetupNewSystem.sh`
**Remaining considerations:**
- Consider implementing GPG signature verification for tagged releases
- Add cryptographic checksums for external downloads within scripts
### ✅ 2. Enforce HTTPS for All Downloads - RESOLVED
**Previous Issue:** HTTP URLs in Dell OMSA and some repository setups
**Status:** All HTTP URLs converted to HTTPS across:
- `provisioning/Dell/Server/omsa.sh` - Ubuntu archive and Dell repo URLs
- `provisioning/legacy/prox7.sh` - Proxmox download URLs
- `provisioning/Modules/RandD/sslStackFromSource.sh` - Apache source URLs
**Remaining considerations:**
- SSL certificate validation is enabled by default in wget/curl
- Consider adding retry logic for certificate failures
### 3. Implement Secrets Management
**Current Issue:** SSH keys committed to repository, no secrets rotation
**Action Required:**
- Deploy Bitwarden CLI or HashiCorp Vault integration
- Remove SSH public keys from repository
- Create secure key distribution mechanism
- Implement key rotation procedures
- Add environment variable support for sensitive data
**Files to secure:**
- `provisioning/ConfigFiles/SSH/AuthorizedKeys/` (entire directory)
- Hard-coded hostnames in various scripts
## 🔶 MEDIUM PRIORITY (Operational Security)
### 4. Add Script Integrity Verification
**Action Required:**
- Generate SHA256 checksums for all scripts
- Create checksum verification function in Framework-Includes
- Add signature verification for external downloads
- Implement rollback capability on verification failure
### 5. Enhanced Error Recovery
**Action Required:**
- Add state tracking for partial deployments
- Implement resume functionality for interrupted installations
- Create system restoration points before major changes
- Add dependency checking before module execution
### 6. Security Testing Framework
**Action Required:**
- Create integration tests for security configurations
- Add compliance validation (CIS benchmarks, STIG)
- Implement automated security scanning post-deployment
- Create test environments for validation
### 7. Configuration Validation
**Action Required:**
- Add pre-flight checks for system compatibility
- Validate network connectivity to required services
- Check for conflicting software before installation
- Verify sufficient disk space and system resources
## 🔹 LOW PRIORITY (Quality Improvements)
### 8. Documentation Enhancement
**Action Required:**
- Create detailed security architecture documentation
- Add troubleshooting guides for common issues
- Document security implications of each module
- Create deployment runbooks for different environments
### 9. Monitoring and Alerting
**Action Required:**
- Add deployment success/failure reporting
- Implement centralized logging for all installations
- Create dashboards for deployment status
- Add alerting for security configuration drift
### 10. User Experience Improvements
**Action Required:**
- Create web-based deployment interface
- Add progress indicators for long-running operations
- Implement dry-run mode for testing configurations
- Add interactive configuration selection
## Implementation Timeline
**✅ COMPLETED:** Item 1 (Secure deployment method)
**✅ COMPLETED:** Item 2 (HTTPS enforcement)
**Week 1:** Item 3 (Secrets management)
**Week 2-3:** Items 4-5 (Operational improvements)
**Month 2:** Items 6-10 (Quality and monitoring)
## Success Criteria
- [ ] No plaintext secrets in repository
- [x] All downloads use HTTPS with verification ✅
- [x] Deployment method is cryptographically secure ✅
- [ ] Automated testing validates security configurations
- [ ] Rollback capability exists for all changes
- [ ] Comprehensive documentation covers security implications
## Resources Needed
- Access to package repository for signed distributions
- GPG key infrastructure for signing
- Secrets management service (Vault/Bitwarden)
- Test environment infrastructure
- Security scanning tools integration
+162
View File
@@ -0,0 +1,162 @@
# Claude TODO - TSYS PFVCluster Automation Tasks
**Purpose:** Actionable items optimized for AI assistant implementation
**Priority:** Critical → High → Medium → Low
## 🚨 CRITICAL (Immediate Security Fixes)
### ✅ RESOLVED: Secure Deployment Method
**Previous Issue:** `curl | bash` deployment method
**Status:** Fixed in README.md - now uses `git clone` + local script execution
### ✅ RESOLVED: Replace HTTP URLs with HTTPS
**Files modified:**
- `provisioning/Dell/Server/omsa.sh` - Converted 11 HTTP URLs to HTTPS (Ubuntu archive, Dell repo)
- `provisioning/legacy/prox7.sh` - Converted 2 HTTP URLs to HTTPS (Proxmox downloads)
- `provisioning/Modules/RandD/sslStackFromSource.sh` - Converted 3 HTTP URLs to HTTPS (Apache sources)
**Status:** All HTTP URLs in active scripts converted to HTTPS. Only remaining HTTP references are in comments and LibreNMS agent files (external dependencies).
### TASK-002: Add Download Integrity Verification
**Create new function in:** `Framework-Includes/VerifyDownload.sh`
**Function to implement:**
```bash
function verify_download() {
local url="$1"
local expected_hash="$2"
local output_file="$3"
curl -fsSL "$url" -o "$output_file"
local actual_hash=$(sha256sum "$output_file" | cut -d' ' -f1)
if [ "$actual_hash" != "$expected_hash" ]; then
print_error "Hash verification failed for $output_file"
rm -f "$output_file"
return 1
fi
print_info "Download verified: $output_file"
}
```
### TASK-003: Create Secure Deployment Script
**Create:** `provisioning/SecureSetupNewSystem.sh`
**Features to implement:**
- GPG signature verification
- SHA256 checksum validation
- HTTPS-only downloads
- Rollback capability
## 🔶 HIGH (Security Enhancements)
### TASK-004: Remove Hardcoded SSH Keys
**Files to modify:**
- `provisioning/ConfigFiles/SSH/AuthorizedKeys/root-ssh-authorized-keys`
- `provisioning/ConfigFiles/SSH/AuthorizedKeys/localuser-ssh-authorized-keys`
- `provisioning/Modules/Security/secharden-ssh.sh:31,40,51`
**Implementation approach:**
1. Create environment variable support: `SSH_KEYS_URL` or `SSH_KEYS_VAULT_PATH`
2. Modify secharden-ssh.sh to fetch keys from secure source
3. Add key validation before deployment
### TASK-005: Add Secrets Management Framework
**Create:** `Framework-Includes/SecretsManager.sh`
**Functions to implement:**
```bash
function get_secret() { } # Retrieve secret from vault
function validate_secret() { } # Validate secret format
function rotate_secret() { } # Trigger secret rotation
```
### TASK-006: Enhanced Preflight Checks
**Modify:** `Framework-Includes/PreflightCheck.sh`
**Add checks for:**
- Network connectivity to required hosts
- Disk space requirements
- Existing conflicting software
- Required system capabilities
## 🔹 MEDIUM (Operational Improvements)
### TASK-007: Add Configuration Backup
**Create:** `Framework-Includes/ConfigBackup.sh`
**Functions:**
```bash
function backup_config() { } # Create timestamped backup
function restore_config() { } # Restore from backup
function list_backups() { } # Show available backups
```
### TASK-008: Implement State Tracking
**Create:** `Framework-Includes/StateManager.sh`
**Track:**
- Deployment progress
- Module completion status
- Rollback points
- System changes made
### TASK-009: Add Retry Logic
**Enhance existing scripts with:**
- Configurable retry attempts for network operations
- Exponential backoff for failed operations
- Circuit breaker for repeatedly failing services
## 🔸 LOW (Quality of Life)
### TASK-010: Enhanced Logging
**Modify:** `Framework-Includes/Logging.sh`
**Add:**
- Structured logging (JSON format option)
- Log levels (DEBUG, INFO, WARN, ERROR)
- Remote logging capability
- Log rotation management
### TASK-011: Progress Indicators
**Add to:** `Framework-Includes/PrettyPrint.sh`
```bash
function show_progress() { } # Display progress bar
function update_status() { } # Update current operation
```
### TASK-012: Dry Run Mode
**Add to:** `provisioning/SetupNewSystem.sh`
**Implementation:**
- `--dry-run` flag support
- Preview of changes without execution
- Dependency analysis output
## Implementation Order for Claude
**Updated Priority After Security Fix (July 14, 2025):**
1. **Start with TASK-001** (HTTPS enforcement - simple find/replace operations)
2. **Create framework functions** (TASK-002, TASK-005, TASK-007)
3. **Enhance existing modules** (TASK-004, TASK-006)
4. **Add operational features** (TASK-008, TASK-009)
5. **Improve user experience** (TASK-010, TASK-011, TASK-012)
**Note:** Major deployment security risk resolved - remaining tasks focus on hardening internal operations.
## File Location Patterns
- **Framework components:** `Framework-Includes/*.sh`
- **Security modules:** `provisioning/Modules/Security/*.sh`
- **Configuration files:** `provisioning/ConfigFiles/*/`
- **Main entry point:** `provisioning/SetupNewSystem.sh`
## Testing Strategy
For each task:
1. Create backup of original files
2. Implement changes incrementally
3. Test with `bash -n` for syntax validation
4. Verify functionality with controlled test runs
5. Document changes made
## Error Handling Requirements
All new functions must:
- Use `set -euo pipefail` compatibility
- Integrate with existing error handling framework
- Log errors to `$LOGFILENAME`
- Return appropriate exit codes
- Clean up temporary files on failure
+95 -9
View File
@@ -1,10 +1,96 @@
# docs/docmap.md
# Documentation Map
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Documentation index — now lives in the Project Overview topic**
>
> **Read it here:** https://community.turnsys.com/t/296
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
> **Index of all documentation in this repo.** Agents must update this file
> whenever a doc is added, removed, or substantively changed.
> **Last updated:** 2026-07-28
## Kubernetes Architecture ([`k8s/`](k8s/))
Distro decision, target architecture, control-plane design, bootstrap and DR
procedures for the pfv-k8s cluster (Talos + vcluster + Keycloak OIDC).
| Document | Description | Last Reviewed |
|----------|-------------|---------------|
| [`k8s/README.md`](k8s/README.md) | Index + TL;DR of all k8s decisions | 2026-07-28 |
| [`k8s/DISTRO-DECISION.md`](k8s/DISTRO-DECISION.md) | Talos vs k3s analysis. Recommendation: Talos, driven by ITAR/classified requirement | 2026-07-28 |
| [`k8s/ARCHITECTURE.md`](k8s/ARCHITECTURE.md) | Target architecture: control plane, network, identity, storage, tenant isolation, bootstrap, DR. Mermaid diagrams | 2026-07-28 |
## Proxmox Cluster ([`proxmox/`](proxmox/))
Fleet operations, hardware, performance tuning, storage architecture.
| Document | Description | Last Reviewed |
|----------|-------------|---------------|
| [`proxmox/PROJECT.md`](proxmox/PROJECT.md) | Comprehensive fleet report: 7 hosts, VM inventory, storage, recommendations | 2026-07-27 |
| [`proxmox/AUDIT-2026-07-28.md`](proxmox/AUDIT-2026-07-28.md) | Fresh fleet audit with current VM placements, storage redundancy analysis, pre-k8s action items | 2026-07-28 |
| [`proxmox/TODO.md`](proxmox/TODO.md) | Pending physical hardware work (tsys2/4/5 Friday plan) | 2026-07-27 |
| [`proxmox/K8S.md`](proxmox/K8S.md) | Kubernetes storage/host analysis (predecessor to [`k8s/`](k8s/)) | 2026-07-27 |
## Server Build ([`server-build/`](server-build/))
Server provisioning, security hardening, DNS/NTP configuration.
| Document | Description | Last Reviewed |
|----------|-------------|---------------|
| [`server-build/SECURITY.md`](server-build/SECURITY.md) | Security architecture: SSH hardening, 2FA, SCAP-STIG, Wazuh, auditd | 2026-07-25 |
| [`server-build/tailscale.md`](server-build/tailscale.md) | Tailscale vs managed DNS analysis (RESOLVED — netinfra pair serves knel.net) | 2026-07-28 |
| [`server-build/DEPLOYMENT.md`](server-build/DEPLOYMENT.md) | Server deployment procedures, package lists, config flow | 2026-07-25 |
| [`server-build/TSYS-2FA-GUIDE.md`](server-build/TSYS-2FA-GUIDE.md) | End-user guide for 2FA setup (SSH, Cockpit, Webmin) | 2026-07-25 |
| [`server-build/DEVELOPMENT-GUIDELINES.md`](server-build/DEVELOPMENT-GUIDELINES.md) | Coding standards, commit conventions, script patterns | 2026-07-25 |
## Operational Guides (outside docs/)
| Document | Description | Last Reviewed |
|----------|-------------|---------------|
| [`../powerman/README.md`](../powerman/README.md) | Cyclades PM10i PDU management via powerman on pfv-tsys1 | 2026-07-28 |
| [`../console/README.md`](../console/README.md) | Serial console management (ser2net + conman) for 7 network switches on pfv-tsys4 | 2026-07-28 |
| [`../k8s/README.md`](../k8s/README.md) | k3s cluster setup scripts: wipe, bootstrap, taint, verify (3-node HA over Tailscale) | 2026-07-28 |
| [`../dns-cluster-setup/README.md`](../dns-cluster-setup/README.md) | Technitium DNS cluster setup: export, deploy, cluster, verify | 2026-07-28 |
| [`../tests/README.md`](../tests/README.md) | Test suite documentation: unit, security, validation tests | 2026-07-28 |
| [`../netinfra/pfv-netinfra-setup.md`](../netinfra/pfv-netinfra-setup.md) | pfv-netinfra-01/02 initial setup guide | 2026-07-27 |
| [`../netinfra/pfv-netboot-setup.md`](../netinfra/pfv-netboot-setup.md) | pfv-netboot reference node setup | 2026-07-27 |
## Archive ([`archive/`](archive/))
Historical AI reviews, completed task lists, and pre-refactor examples. Read-only
context — do not update; link to active docs instead.
| Document | Description |
|----------|-------------|
| [`archive/CODE-REVIEW-FINDINGS.md`](archive/CODE-REVIEW-FINDINGS.md) | Early code review findings (most issues now fixed) |
| [`archive/REFACTORING-EXAMPLES.md`](archive/REFACTORING-EXAMPLES.md) | Pre-refactor code patterns (historical "before" examples) |
| [`archive/Claude-Review.md`](archive/Claude-Review.md) | Claude's initial code review |
| [`archive/AIReview-QWEN.md`](archive/AIReview-QWEN.md) | Qwen AI review |
| [`archive/AiOverview-Gemini.md`](archive/AiOverview-Gemini.md) | Gemini project overview |
| [`archive/AiOverview-OpenCode.md`](archive/AiOverview-OpenCode.md) | OpenCode project overview |
| [`archive/AiSecurityAudit-Gemini.md`](archive/AiSecurityAudit-Gemini.md) | Gemini security audit |
| [`archive/charles-todo.md`](archive/charles-todo.md) | Charles's early task list (completed) |
| [`archive/claude-todo.md`](archive/claude-todo.md) | Claude's early task list (completed) |
## Top-Level Files
| File | Description |
|------|-------------|
| [`../AGENTS.md`](../AGENTS.md) | Agent operating instructions (repo layout, git policy, gardening protocol) |
| [`../STATUS.md`](../STATUS.md) | Living project status (agent-maintained, human read-only) |
| [`../README.md`](../README.md) | Project overview and quick start |
| [`../LICENSE`](../LICENSE) | License |
---
## Agent Gardening Protocol
When making changes to this repo, agents MUST:
1. **Update [`../STATUS.md`](../STATUS.md)** if the work changes infrastructure
state, completes/starts a task, or discovers a new issue.
2. **Update this file (`docmap.md`)** if a doc is added, removed, or has a
substantive content change. Update the "Last Reviewed" date.
3. **Verify cross-references** — any new `.md` file must be linked from at
least one existing doc or this map.
4. **Check for stale paths** — after any directory rename or file move,
`grep -rn 'old/path' --include='*.md'` and fix all references.
5. **Keep code and docs in sync** — if you change a script's interface,
behavior, or location, update every doc that references it in the same
commit.
+673
View File
@@ -0,0 +1,673 @@
# pfv-k8s Target Architecture (Talos)
> **Companion to:** [`DISTRO-DECISION.md`](DISTRO-DECISION.md) (why Talos),
> [`../proxmox/K8S.md`](../proxmox/K8S.md) (storage/host analysis from the
> Proxmox audit).
**Last updated:** 2026-07-28
---
## Table of Contents
1. [High-Level Architecture](#1-high-level-architecture)
2. [Control Plane](#2-control-plane)
3. [Network Topology](#3-network-topology)
4. [CNI: Cilium](#4-cni-cilium)
5. [Identity and Trust](#5-identity-and-trust)
6. [Tenant Isolation (vcluster)](#6-tenant-isolation-vcluster)
7. [Storage Integration](#7-storage-integration)
8. [Local Image Registry](#8-local-image-registry)
9. [Bootstrap Procedure](#9-bootstrap-procedure)
10. [Disaster Recovery](#10-disaster-recovery)
11. [Migration from Current State](#11-migration-from-current-state)
---
## 1. High-Level Architecture
```mermaid
flowchart TB
subgraph RESIDENCE["Residence — Proxmox LAN"]
subgraph CP["Talos Control Plane (3 cnodes)"]
C1[cnode1<br/>tsys9 · local-SSD]
C2[cnode2<br/>tsys9 · local-SSD]
C3[cnode3<br/>tsys1 · local-HDD]
end
subgraph WP["Talos Worker Plane"]
W3[wnode-tsys3<br/>NVMe · 28GB]
W5[wnode-tsys5<br/>NVMe · 32-64GB]
W6[wnode-tsys6<br/>NFS-HDD · 64-96GB]
W7[wnode-tsys7<br/>NFS-HDD · 96-128GB]
W9[wnode-tsys9<br/>local-SSD · 4-8GB]
end
ETCD[(etcd<br/>raft, mTLS)]
REG[(Harbor registry<br/>on D3 SSD · tsys5)]
BASTION[tailscale-router VM<br/>subnet router]
end
subgraph TAILNET["Tailscale overlay"]
OP[Operator devices]
end
subgraph CLOUDRON["Cloudron production — Reston VA"]
KC[Keycloak OIDC IdP]
end
C1 ---|mTLS LAN| ETCD
C2 ---|mTLS LAN| ETCD
C3 ---|mTLS LAN| ETCD
CP -->|pull images| REG
WP -->|pull images| REG
OP -->|Talos API :50000<br/>via subnet route| BASTION
BASTION -.->|LAN| CP
CP -->|OIDC| KC
WP -->|OIDC| KC
classDef talos fill:#1a1a2e,stroke:#e94560,color:#fff
classDef infra fill:#0f3460,stroke:#e94560,color:#fff
classDef external fill:#16213e,stroke:#533483,color:#fff
class CP,WP,ETCD talos
class REG,BASTION infra
class OP,KC,EXTERNAL external
```
### Design principles
1. **LAN-only cluster nodes.** Zero internet egress from cnodes/wnodes.
Strongest posture for ITAR/classified.
2. **Admin via Tailscale subnet router.** Existing `tailscale-router` VM
advertises the cluster LAN subnet. Operator reaches Talos API from
anywhere.
3. **Local-first storage.** Cnodes boot from local disk (no NFS dependency
for etcd). Workers boot from local disk where available; NFS for bulk
data only.
4. **Per-tenant vcluster.** Workload isolation via virtual clusters on top
of the Talos host cluster.
5. **OIDC everywhere.** Talos API and Kubernetes API both trust Keycloak
tokens. No long-lived static credentials for humans.
---
## 2. Control Plane
### 2.1 Recommendation: 3 cnodes (down from 5)
| Option | Quorum | Failure tolerance | etcd write cost | Resource cost |
|--------|--------|-------------------|-----------------|---------------|
| **3 cnodes** (recommended) | 2 of 3 | Tolerates **1** failure | Lower (faster commits) | 3 × (2c/4GB/32GB) = 6c / 12GB |
| 5 cnodes (current plan) | 3 of 5 | Tolerates **2** failures | Higher | 5 × (2c/4GB/32GB) = 10c / 20GB |
For a solo-operated R&D cluster, **3 cnodes is the HA standard**. The
failure-tolerance jump from 1→2 rarely justifies the doubled etcd write
quorum and the extra 4GB/2c per cnode. The 2 freed VM slots (and their
host capacity) are better spent on tenant worker allocations.
**Caveat:** if your ITAR/classified accreditation counsel mandates 2-failure
tolerance on the control plane, keep 5. Otherwise 3.
### 2.2 Cnode placement
Per [`../proxmox/K8S.md`](../proxmox/K8S.md) §4.3, cnodes should use
**local-lvm boot disks** so etcd has no NFS dependency. Concrete placement:
| cnode | Host | Boot disk | Type | Why |
|-------|------|-----------|------|-----|
| cnode1 | tsys9 | local-lvm (PNY CS900 SSD) | LOCAL-SSD | Fastest available for etcd. |
| cnode2 | tsys9 | local-lvm (PNY CS900 SSD) | LOCAL-SSD | Same host, different disk OK (host failure is the failure domain, not disk). |
| cnode3 | tsys1 | local-lvm (HDD) | LOCAL-HDD | Host diversity. Slower than SSD but no NFS hop. |
**Quorum survival:**
| Failure | cnodes lost | Quorum OK? |
|---------|-------------|------------|
| tsys9 host dies | cnode1 + cnode2 | NO (1 of 3) — would need 4th cnode elsewhere, or accept this risk. |
| tsys1 host dies | cnode3 | YES (2 of 3) |
| Any storage server dies | 0 | YES (3 of 3) — local disks unaffected |
**Refinement:** putting both SSD cnodes on tsys9 means tsys9 host failure
loses quorum. Alternative: spread cnodes across 3 different hosts. See
"open question" at end of this section.
### 2.3 Machine config strategy
Talos nodes are configured by **machine configs** (YAML). Two flavors:
- **`controlplane.yaml`** — for cnodes. Enables etcd, scheduler,
controller-manager, API server.
- **`worker.yaml`** — for wnodes. Joins cluster, runs kubelet + containerd.
Strategy for this cluster:
1. **One shared `talosconfig`** (client identity) — stored in 1Password
and in the Proxmox Backup Server (PBS) encrypted backup target.
2. **Per-node machine config patches** — small patches on top of the base
`controlplane.yaml` / `worker.yaml` for node-specific settings:
- Hostname
- Network interface + IP (DHCP or static — recommend static for cnodes)
- Schematic image digest (pinned Talos version)
- System extensions (e.g., `tailscale` — only if running Pattern A
instead of recommended Pattern C)
3. **All machine configs in Git** under a future `k8s/talos-configs/`
directory. Secrets are templated in at apply-time from 1Password / sops.
```mermaid
flowchart LR
BASE[base controlplane.yaml] --> PATCH1[patch: cnode1]
BASE --> PATCH2[patch: cnode2]
BASE --> PATCH3[patch: cnode3]
BASEW[base worker.yaml] --> PATCHW[patch: per-wnode]
PATCH1 --> APPLY1[talosctl apply]
PATCH2 --> APPLY2[talosctl apply]
PATCH3 --> APPLY3[talosctl apply]
PATCHW --> APPLYW[talosctl apply]
```
### 2.4 Open question: cnode host spread
If you accept "tsys9 failure = quorum loss" as a tolerable risk (solo R&D
cluster, tsys9 is brand-new hardware, single digit annual failure
probability), the layout in §2.2 is fine.
If not, alternative spread across 3 hosts:
| cnode | Host | Boot disk |
|-------|------|-----------|
| cnode1 | tsys9 | local-lvm SSD |
| cnode2 | tsys1 | local-lvm HDD |
| cnode3 | tsys3 | local-lvm NVMe |
tsys3's local-lvm is **349 GB Samsung PM961 NVMe** (per
[`../proxmox/PROJECT.md`](../proxmox/PROJECT.md) §3.3) — currently unused,
would make an excellent etcd disk.
**This 3-host spread survives any single host failure with quorum intact.
Recommended.**
---
## 3. Network Topology
### 3.1 Zones
```mermaid
flowchart TB
subgraph INTERNET["Internet"]
FIBER[Gigabit symmetric fiber]
end
subgraph RESLAN["Residence LAN 192.168.x.x/24"]
subgraph CLUSTERNET["Cluster nodes — LAN only, no egress"]
CNODES[Cnodes 192.168.3.x]
WNODES[Wnodes 192.168.3.x]
end
BASTION[tailscale-router<br/>192.168.3.x + 100.x.x.x]
REG[Harbor registry<br/>192.168.3.x]
STORAGE[NFS servers<br/>tsys4, tsys5]
end
subgraph TSNET["Tailscale 100.x.x.x/8"]
OPS[Operator devices]
KC[Keycloak<br/>via Cloudron prod]
end
FIBER --> BASTION
BASTION <-. subnet route .-> CLUSTERNET
OPS -->|TCP 50000 talos API| BASTION
BASTION -->|LAN forward| CNODES
CNODES -->|LAN mTLS| WNODES
CNODES -->|OIDC HTTPS| KC
WNODES -->|pull images| REG
WNODES -->|bulk data IO| STORAGE
CNODES -->|pull images| REG
```
### 3.2 Address plan (suggested)
Reserve a small contiguous block in the residence LAN for cluster nodes:
| Role | Range | Count |
|------|-------|-------|
| Cnodes | `192.168.3.31-33` | 3 |
| Wnodes | `192.168.3.41-49` | up to 9 (1 per Proxmox host + spare) |
| Bastion | existing `tailscale-router` | 1 |
| Registry | `192.168.3.50` | 1 (Harbor) |
**Static IPs are strongly recommended for cnodes** (etcd cluster membership
is hostname-based; stable IPs make `talosctl` targeting simple). Workers
can DHCP.
### 3.3 Firewall posture
Each cnode/wnode has:
- **Ingress** from LAN: TCP 50000 (Talos API), TCP 6443 (Kubernetes API on
cnodes only), plus CNI ports (varies by CNI — see §4).
- **Ingress** from Tailscale: none (cluster nodes are not on Tailscale).
- **Egress:** LAN-only. Block all RFC1918-external traffic at the perimeter
firewall for these IPs. ITAR workloads must not be able to phone home.
The bastion runs Tailscale and forwards TCP 50000/6443 to cluster nodes
via the subnet route.
---
## 4. CNI: Cilium
**Recommendation: Cilium** (eBPF-based CNI).
| Property | Why it matters here |
|----------|---------------------|
| **NetworkPolicy** (incl. L7) | Per-tenant isolation rules in vclusters. |
| **Node-to-node encryption** | WireGuard-based IPSec replacement. All inter-node pod traffic is encrypted on the wire. **Important for ITAR tenants.** |
| **Hubble** | Observable flows — forensic record of which pod talked to which. Useful for compliance evidence. |
| **No kube-proxy** | Cilium replaces kube-proxy with eBPF. Smaller attack surface on each node. |
| **Talos integration** | First-class. Talos docs document the install path. |
Cilium is deployed via Helm after cluster bootstrap. Node-to-node encryption
enabled. Default-deny NetworkPolicy applied per namespace.
---
## 5. Identity and Trust
### 5.1 Trust flow
```mermaid
sequenceDiagram
autonumber
participant Human as Operator
participant TAIL as Tailscale
participant BAST as Bastion
participant TALOS as Talos API :50000
participant KC as Keycloak (Cloudron)
participant K8S as Kubernetes API :6443
Human->>TAIL: Authenticate (device + SSO)
TAIL-->>Human: Tailnet IP
Human->>BAST: Reach bastion via tailnet
BAST->>TALOS: Forward to LAN node :50000
Human->>KC: OIDC login (browser)
KC-->>Human: Bearer token (short-lived)
Human->>TALOS: talosctl (mTLS with client cert)
Human->>K8S: kubectl --oidc (Keycloak token)
K8S->>KC: Validate token (introspection)
KC-->>K8S: Valid + claims
K8S-->>Human: Authorized response
```
### 5.2 Two distinct identity layers
| Layer | Mechanism | Audience |
|-------|-----------|----------|
| **Talos API** (node ops) | Mutual TLS with client certificate generated from the Talos secrets bundle. | Operators (automation + humans). |
| **Kubernetes API** (kubectl) | OIDC bearer token from Keycloak. RBAC maps group claims → ClusterRole. | Humans. Service accounts use projected tokens (no OIDC). |
The **Talos secrets bundle** is the root of trust for the cluster. Lose it
and you cannot operate the cluster; an attacker with it owns the cluster.
Storage:
1. **Primary:** 1Password (or equivalent) — operator-accessible.
2. **Backup:** PBS encrypted backup target on tsys4 (existing infra).
3. **NOT in Git.** Machine configs go in Git; secrets stay out.
### 5.3 Keycloak client configuration
On Cloudron-hosted Keycloak, register a client `pfv-k8s-talos`:
- **Authorization Code + PKCE flow** (no implicit, no password).
- **Redirect URIs:** `http://localhost:8000` (kubectl oidc-login) + Sidero
Omni/Rancher URLs if/when those are added.
- **Group claims:** `k8s-admin`, `k8s-readonly`, `k8s-tenant-itar`,
`k8s-tenant-rackrental`, etc. These map to Kubernetes RBAC `ClusterRoleBinding`.
---
## 6. Tenant Isolation (vcluster)
### 6.1 Why vcluster
[vcluster](https://www.vcluster.com/) runs a **virtual Kubernetes control
plane** (API server, scheduler, controller-manager, etcd) inside a namespace
of the host cluster. Tenant workloads run on the host's worker nodes but
are isolated by:
- Separate API server (tenant cannot see host cluster objects).
- Separate RBAC and admission control.
- Separate network policies (per-namespace).
- Separate resource quotas.
This aligns with the user's per-tenant plan from
[`../proxmox/K8S.md`](../proxmox/K8S.md) §1.
### 6.2 Tenant registry
| Tenant | Compliance | Workload example | vcluster name |
|--------|-----------|------------------|---------------|
| RackRental | None (internal R&D) | containerlab topology tests | `vc-rackrental` |
| Suborbital non-ITAR | EAR/ITAR-aware but unclassified | Payload telemetry processing | `vc-suborbital-open` |
| Suborbital ITAR | **ITAR-controlled** | Firmware build for USML items | `vc-suborbital-itar` |
| Starting Line Productions | Commercial | Customer media pipeline | `vc-slp` |
```mermaid
flowchart TB
subgraph HOST["Talos host cluster"]
CP[Host control plane<br/>3 cnodes · etcd · Keycloak RBAC]
subgraph NS["Host cluster namespaces"]
NS_RR[ns: vc-rackrental]
NS_SO[ns: vc-suborbital-open]
NS_SI[ns: vc-suborbital-itar]
NS_SLP[ns: vc-slp]
end
end
subgraph VRR["vcluster: vc-rackrental"]
API_RR[k8s API + etcd]
end
subgraph VSO["vcluster: vc-suborbital-open"]
API_SO[k8s API + etcd]
end
subgraph VSI["vcluster: vc-suborbital-itar"]
API_SI[k8s API + etcd]
end
subgraph VSLP["vcluster: vc-slp"]
API_SLP[k8s API + etcd]
end
CP --> NS_RR & NS_SO & NS_SI & NS_SLP
NS_RR --> API_RR
NS_SO --> API_SO
NS_SI --> API_SI
NS_SLP --> API_SLP
classDef itar fill:#3a0000,stroke:#ff0000,color:#fff
class NS_SI,API_SI itar
```
### 6.3 ITAR enforcement at host layer
For the ITAR tenant (`vc-suborbital-itar`), enforce additional host-layer
controls:
- **Node taint** `workload=itar:NoSchedule` on worker nodes dedicated to
ITAR workloads (subset of wnodes, marked in node labels).
- **NetworkPolicy** default-deny egress for the `vc-suborbital-itar`
namespace. Allow only explicit destinations (registry, NFS for ITAR
data tier, Keycloak).
- **Storage isolation:** ITAR PVCs target a dedicated NFS export (e.g.,
`D3-itar` on tsys5) that no other tenant can mount.
- **Audit:** Hubble flows + auditd on the host worker nodes capture all
access to ITAR data.
Rancher (or Sidero Omni) sits above this, presenting each tenant's
vcluster as a separate "cluster" in its UI, with Keycloak SSO gating
access per tenant group claim.
---
## 7. Storage Integration
Per [`../proxmox/K8S.md`](../proxmox/K8S.md) §6. Three StorageClasses:
| StorageClass | Provisioner | Backing | Speed | Use |
|--------------|------------|---------|-------|-----|
| `local-fast` | local-path | wnode local disk (NVMe/SSD/HDD depending on host) | 100-3500 MB/s | Container runtime, scratch, ephemeral |
| `nfs-hdd` | nfs.csi.k8s.io | tsys4 D2/D5, tsys5 S1-S4 | 80-120 MB/s | Bulk data, weather/GIS datasets |
| `nfs-ssd` | nfs.csi.k8s.io | tsys5 D3, tsys5 T5-SSD | 200-400 MB/s | Latency-sensitive persistent data |
### 7.1 CSI driver notes
- **NFS CSI:** [`csi-driver-nfs`](https://github.com/kubernetes-csi/csi-driver-nfs)
(CNCF sandbox). Deploys via Helm. Each StorageClass points at a specific
NFS server + base export path.
- **local-path:** Rancher Local Path Provisioner. Single-binary, deploys
with one manifest. Uses wnode's kubelet root dir.
### 7.2 ITAR data isolation
The ITAR tenant should target a dedicated NFS export, not shared
`nfs-hdd`. Recommended:
- Allocate `S4` on tsys5 (currently 99% empty, 435 GB free) as
`nfs-itar` StorageClass. Mountable only from `vc-suborbital-itar`
namespace via RBAC + NetworkPolicy.
---
## 8. Local Image Registry
### 8.1 Recommendation: Harbor on D3 SSD
D3 SSD (tsys5, post-Friday SAS relocation) is 445 GB and 99% empty. Use it
for a **Harbor** instance:
| Property | Value |
|----------|-------|
| **Storage** | D3 SSD on tsys5 (NFS export, fast tier) |
| **VM** | New VM `pfv-registry` on tsys5, local-nonprod boot, D3 data |
| **Function** | (a) Pull-through cache for Docker Hub / Quay / gcr.io<br/>(b) Host private images<br/>(c) Cosign image signing verification |
| **Exposure** | LAN-only. `192.168.3.50:443`. Not exposed to internet. |
### 8.2 Pull-through cache benefit
Cluster nodes have zero internet egress (per §3.3). Without a local cache,
image pulls fail. With Harbor as a pull-through cache:
```mermaid
sequenceDiagram
WNODE->>HARBOR: docker pull nginx:1.25
alt cache hit
HARBOR-->>WNODE: layer bytes (LAN-speed)
else cache miss
HARBOR->>DOCKERHUB: pull nginx:1.25 (egress)
DOCKERHUB-->>HARBOR: layer bytes
HARBOR-->>WNODE: layer bytes (cached for next time)
end
```
Cluster nodes pull from Harbor over LAN (gigabit). Harbor is the only
machine in the cluster with container-registry internet egress, and that
egress can be locked to specific upstreams (docker.io, quay.io, gcr.io,
ghcr.io).
### 8.3 Supply-chain integrity (future)
Harbor + Cosign lets you require that all images deployed to the ITAR
tenant are signed by a trusted key. This is a strong ITAR/CISA-attestation
control. Implementation deferred to a later session.
---
## 9. Bootstrap Procedure
### 9.1 One-time setup
```mermaid
sequenceDiagram
autonumber
participant OP as Operator
participant GIT as Git repo
participant ONEPW as 1Password
participant PBS as PBS (tsys4)
OP->>GIT: Clone PFVCluster repo
OP->>ONEPW: Generate Talos secrets bundle (offline)
ONEPW-->>OP: secrets.yaml
OP->>PBS: Backup secrets.yaml (encrypted)
OP->>GIT: Write machine configs (no secrets)
```
### 9.2 Provision first cnode (bootstrap)
```mermaid
sequenceDiagram
autonumber
participant OP as Operator
participant PX as Proxmox host
participant C1 as cnode1
participant ETCD as etcd (new)
OP->>PX: qm create VM (Talos QCOW2 disk, local-lvm)
OP->>PX: qm start VMID
C1->>C1: Boots Talos (no config yet, "maintenance mode")
OP->>C1: talosctl apply --patch cnode1.yaml (with secrets)
C1->>C1: Applies config, restarts services
OP->>C1: talosctl bootstrap
C1->>ETCD: Initialize single-node raft
ETCD-->>C1: ready
OP->>C1: talosctl kubeconfig (fetch admin kubeconfig)
OP->>C1: talosctl etcd snapshot (initial backup → PBS)
```
### 9.3 Add second and third cnodes
```mermaid
sequenceDiagram
autonumber
participant OP as Operator
participant PX as Proxmox host
participant C2 as cnode2
participant C3 as cnode3
participant C1 as cnode1 (existing)
OP->>PX: qm create + start cnode2 VM
C2->>C2: Boots Talos maintenance mode
OP->>C2: talosctl apply --patch cnode2.yaml
C2->>C1: Join etcd cluster
OP->>PX: qm create + start cnode3 VM
C3->>C3: Boots Talos maintenance mode
OP->>C3: talosctl apply --patch cnode3.yaml
C3->>C1: Join etcd cluster
Note over C1,C3: etcd now has 3/3 members → HA quorum
```
### 9.4 Post-bootstrap cluster configuration
Once 3 cnodes are up and joined:
1. **Install Cilium** (CNI) via Helm. Enable node-to-node encryption.
2. **Install CSI drivers** — nfs-csi + local-path provisioner.
3. **Create StorageClasses**`local-fast`, `nfs-hdd`, `nfs-ssd`.
4. **Deploy Harbor** on the `pfv-registry` VM, exposed at `192.168.3.50`.
5. **Configure Kubernetes API OIDC** — Keycloak client (§5.3).
6. **Apply default-deny NetworkPolicy** in all namespaces.
7. **Install vcluster CLI** + create 4 tenant vclusters (§6).
8. **First etcd snapshot** + automated daily snapshot cron → PBS.
### 9.5 Add workers
Workers are simpler (no etcd):
```mermaid
sequenceDiagram
OP->>PX: qm create + start wnode-X VM (Talos QCOW2)
WNODE->>WNODE: Boots maintenance mode
OP->>WNODE: talosctl apply --patch worker-X.yaml
WNODE->>C1: Kubelet registers with API server
C1-->>WNODE: Approved (auto via bootstrap token)
Note over WNODE: Joins cluster, becomes Ready
```
---
## 10. Disaster Recovery
### 10.1 Backup strategy
| Artifact | Frequency | Storage | Tool |
|----------|-----------|---------|------|
| **Talos secrets bundle** | Once (regen only on rotation) | 1Password + PBS (encrypted) | Manual |
| **Machine configs** | Continuous (Git) | Git remote + PBS | Git |
| **etcd snapshot** | Daily + before each change | PBS (tsys4 SMR target, 4.3 TB free) | `talosctl etcd snapshot` |
| **vcluster etcd** | Daily per vcluster | PBS | `kubectl exec ... etcdctl snapshot` |
| **Harbor metadata** | Daily | PBS | Harbor built-in backup |
### 10.2 Restore scenarios
**Lost 1 cnode (e.g., tsys9 disk failure):**
1. Provision new VM on tsys9 (or other host with local SSD).
2. Apply cnode2 machine config patch.
3. New cnode joins etcd, syncs state from survivors.
4. Quorum was never lost (2 of 3 alive throughout).
**Lost 2 cnodes simultaneously (quorum lost):**
1. Use surviving cnode's etcd snapshot.
2. Provision 3 new cnode VMs.
3. On first: `talosctl bootstrap --recover-from=snapshot.db`.
4. Join other 2 cnodes.
5. Workers reconnect automatically once API server is back.
**Total cluster loss (all 3 cnodes):**
1. Restore from latest PBS etcd snapshot.
2. Provision new cnode VMs.
3. `talosctl bootstrap --recover-from=snapshot.db`.
4. Re-join workers.
5. Verify tenant vclusters restored.
### 10.3 Recovery time objectives
| Scenario | RTO | RPO |
|----------|-----|-----|
| Single cnode failure | < 30 min | 0 (no data loss) |
| Quorum loss (2 cnodes) | < 2 hours | ≤ 24 hours (last snapshot) |
| Total cluster loss | < 4 hours | ≤ 24 hours |
---
## 11. Migration from Current State
### 11.1 Current state
- 5 cnode VMs exist (Debian stock + Tailscale).
- **No k3s deployed yet.** Cluster was never bootstrapped.
- 6 wnode VMs exist (some stopped).
- No workloads running in k8s.
### 11.2 Migration: clean cutover (not a migration)
Since there is no etcd data and no workloads to preserve, the path is a
**clean rebuild**:
| Phase | Action | Risk |
|-------|--------|------|
| **0. Prep** | Generate Talos secrets. Store in 1Password + PBS. Write machine configs to Git. | Low. |
| **1. Bootstrap 3 new cnodes** | Build 3 NEW Talos cnode VMs (not the existing 5). Use local-lvm boot disks (tsys9 × 2, tsys3 × 1 per §2.4 recommended spread). | Low. Existing Debian cnodes can keep running idle. |
| **2. Configure cluster** | Install Cilium, CSI, StorageClasses, OIDC, Harbor. | Low. |
| **3. Add workers** | Re-image existing wnode VMs as Talos, or build new ones. | Low. No workloads to drain. |
| **4. Decommission old Debian cnodes** | Once cluster is stable, shut down + delete the 5 old Debian cnode VMs. | Low. |
| **5. Tenant vclusters** | Stand up per-tenant vclusters. | Medium (policy tuning). |
### 11.3 Open dependency: Friday hardware work
Phases 1-2 require:
- **tsys3 local-lvm available.** Per
[`../proxmox/PROJECT.md`](../proxmox/PROJECT.md) §3.3, tsys3 has 349 GB
free NVMe local-lvm. Currently unused. **Ready.**
- **tsys9 local-lvm available.** 136 GB PNY CS900 SSD. **Ready.**
- **D3 SSD relocated to tsys5 SAS.** Currently USB on tsys4. Per
[`../proxmox/TODO.md`](../proxmox/TODO.md) §2, scheduled for Friday.
Harbor depends on D3 being available on tsys5.
Bootstrap of the cnodes does NOT depend on Friday hardware work. Only the
Harbor registry does.
---
## Appendix: Open questions for next session
1. **Confirm 3 vs 5 cnodes** (§2.1). Recommendation: 3.
2. **Confirm cnode host spread** (§2.4). Recommendation: 3-host spread
(tsys9, tsys1, tsys3).
3. **Static IPs for cnodes** (§3.2). Recommendation: yes, `192.168.3.31-33`.
4. **Rancher vs Sidero Omni** for cluster management UI. Both viable.
Defer until cluster is up.
5. **Subnet router ACL approval** on Tailscale admin console (§3). Needs
approval of `192.168.3.0/24` route advertisement.
6. **ITAR worker node subset** (§6.3). Which wnodes are tainted for ITAR?
Recommendation: tsys6 + tsys7 (heaviest hosts, NFS-only boot) as
general capacity; tsys3 + tsys5 (local fast storage) reserved for
non-ITAR HPC.
+188
View File
@@ -0,0 +1,188 @@
# Distro Decision: Talos Linux vs k3s
> **Recommendation: Talos Linux.**
> The k3s-on-Debian plan was sound before the ITAR/classified requirement
> entered scope. Once classified workloads are on the table, Talos's
> immutable, API-only, measured-boot-capable posture is materially easier
> to certify and defend.
**Last updated:** 2026-07-28
---
## 1. Decision context
| Factor | Constraint |
|--------|-----------|
| **Workload class** | R&D + RackRental (containerlab) + **ITAR / classified** suborbital workloads + commercial (Starting Line Productions) |
| **Compliance drivers** | ITAR (USML categories), possible classified handling (NIST 800-171, CNSSI 1253) |
| **Hardware** | 7 standalone Proxmox hosts (no `pvecm`), managed via PDM. Live migration NOT available — disk moves via Proxmox "Storage Migrate" UI. |
| **Network** | Gigabit symmetric fiber to residence. LAN-only cluster traffic desirable. Tailscale already in use (overlay for admin access). |
| **Current cnode state** | Stock Debian VMs joined to Tailscale. **No k8s distribution has been deployed yet.** Clean cutover possible. |
| **Operations** | Solo founder. Must be reproducible from Git, low-touch, low-debug-overhead. |
---
## 2. Head-to-head comparison
### 2.1 ITAR / classified posture
| Property | Talos Linux | k3s on Debian |
|----------|-------------|---------------|
| **Node OS mutability** | Immutable rootfs (squashfs, read-only). Reboot returns to known-good state. | Mutable. `apt install`, file edits persist. |
| **Shell / SSH access** | **None.** No SSH daemon, no shell, no `kubectl debug node` shell. | Full SSH + bash. STIG hardening reduces (does not eliminate) attack surface. |
| **Operational surface** | Single gRPC API (mTLS, signed certs, audit log) on port 50000. | SSH + kubelet API + etcd API + package manager + cron + systemd + userland. |
| **Measured boot** | Supported. TPM attestation can prove the node booted the signed Talos image you pinned. | Possible but bolt-on; auditors will ask why you didn't disable the bootloader first. |
| **Configuration provenance** | Entire node state is a YAML machine config in Git. `talosctl apply` is the only mutation path. | Config drift via SSH edits, package updates, manual service restarts. STIG/CAT-IV findings multiply. |
| **Supply chain** | Every Talos release is a signed artifact (cosign). Pin by image digest. | Debian package provenance is good but the surface is enormous (~30K packages in a base install). |
| **Forensic readiness** | API log + kernel log + Talos event log = sufficient for "what ran, when, with what config." | Same possible but requires explicit configuration to be trustworthy. |
| **STIG / CIS conformance** | Intrinsically close. Talos publishes CIS benchmark results per release. | Requires running SCAP-STIG (already in this repo) and remediating findings continuously. |
**Bottom line:** For classified workloads, an auditor's first question is
"how do you prevent unauthorized changes to a node?" Talos's answer is
"the OS is immutable and the only path is a signed API call." k3s's answer
is"SSH is locked down and we scan with STIG." The first is structurally
stronger; the second is operationally maintained.
### 2.2 Operational considerations
| Property | Talos | k3s |
|----------|-------|-----|
| **Familiarity** | New model (`talosctl apply`, no SSH). Learning curve. | Stock Debian + k3s binary. Familiar. |
| **Debugging** | `talosctl logs`, `talosctl dmesg`, `talosctl dashboard`. No shell. | `ssh`, `journalctl`, `crictl`. Full shell. |
| **Tailscale integration** | System extension (`siderolabs/tailscale`). Stable since Talos 1.3. | Native — `apt install tailscale`. Zero friction. |
| **Backup / DR** | `talosctl etcd snapshot` (one command). Cluster can be restored from snapshot + machine configs. | DIY (`etcdctl snapshot` + manual cert management). |
| **Upgrades** | `talosctl upgrade` — atomic, automated rollback on health-check failure. | Manual: drain, `k3s` package update, reboot, uncordon. |
| **Proxmox compatibility** | QCOW2 image boots natively on KVM/QEMU. virtio-net, virtio-scsi, virtio-rng all supported. | Same. |
| **Ecosystem maturity** | Production-grade. Sidero (the company) offers Omni (managed control plane for Talos). | Production-grade. Rancher (SUSE) backs it. |
### 2.3 Cost of choosing Talos over the existing k3s plan
The cnodes are currently **stock Debian VMs joined to Tailscale**. Critically,
**no k3s cluster has been deployed yet** — k3s was only the *plan*. Therefore:
- **No etcd data to migrate.** Clean cutover, not a migration.
- **No workloads to drain.** The cluster is empty.
- **Cnode VMs get re-imaged** with Talos QCOW2 (or rebuilt from scratch —
either way it's a `qm` script, not a stateful migration).
- **Tailscale config shifts** from "installed via apt" to "Talos system
extension." (Or, per our recommendation in
[`ARCHITECTURE.md`](ARCHITECTURE.md) §3, **Tailscale moves off the cluster
nodes entirely** and onto the existing `tailscale-router` bastion as a
subnet router. Cluster nodes become LAN-only.)
**Net cost:** rebuilding 3 cnode VMs as Talos + writing ~200 lines of
machine config YAML. The hardening investment already encoded in
`provisioning/Modules/Security/` is **not wasted** — it still applies to
every non-cluster VM (netinfra, UCS, LibreNMS, SIEM, bastion, etc.). Only
the cnodes/wnodes move to Talos.
---
## 3. Tailscale compatibility (deep-dive)
Tailscale on Talos is well-supported but introduces a configuration dimension
worth being explicit about. Three patterns exist:
### Pattern A — Tailscale on every cluster node (what you have now, on Debian)
Each cnode/wnode runs `tailscaled` and joins the tailnet. Cluster nodes have
internet egress (to Tailscale DERP servers and for coordinate).
- **Talos implementation:** add `siderolabs/tailscale` system extension to
each machine config, configure `machine.network.interfaces`.
- **Pros:** Operator can hit any node's Talos API from any Tailscale device.
- **Cons:** Cluster nodes have internet egress. For ITAR workloads, this is a
finding (data exfiltration path).
### Pattern B — Tailscale on bastion only, SSH/API jump
Cluster nodes are LAN-only. Operator Tunnels to bastion (existing
`tailscale-router` VM), then runs `talosctl` from the bastion.
- **Pros:** Zero internet egress from cluster nodes.
- **Cons:** Two-step access. Bastion must run recent `talosctl`. Each operator
action originates from the bastion (auditable but clunky).
### Pattern C — Tailscale subnet router on bastion (recommended)
The existing `tailscale-router` VM advertises the cluster LAN subnet
(e.g. `192.168.3.0/24`) into the tailnet as a **subnet route**. Operator's
Tailscale client transparently routes cluster-bound traffic through the
bastion. From the operator's workstation, `talosctl --nodes 192.168.3.x`
"just works."
- **Pros:**
- Cluster nodes have **zero internet egress** (strongest ITAR posture).
- Operator UX is unchanged from direct LAN access.
- All access is mediated by Tailscale's identity + ACLs (already integrated
with your env).
- Audit trail lives in Tailscale + bastion logs.
- **Cons:**
- Bastion becomes a dependency for remote admin (LAN-local admin still
works without it).
- Must enable IP forwarding + subnet route approval in Tailscale ACLs.
**Recommendation: Pattern C.** Documented in
[`ARCHITECTURE.md`](ARCHITECTURE.md) §3.
---
## 4. Recommendation
**Deploy Talos Linux** as the k8s distribution for `pfv-k8s`.
### Justification
1. **Compliance posture is structural, not operational.** "Immutable,
API-only, measured-boot" is a property of Talos itself; "STIG-hardened"
is a property of how Debian is operated. The first is dramatically
easier to argue to an ITAR counsel or classified accreditation officer
(DSS, DCSA) than the second.
2. **Zero migration cost.** The k3s cluster was never deployed. Reimaging
3 cnodes with Talos is a `qm` script invocation, not a stateful
migration. The sunk cost of "we planned k3s" is **zero deployed state**.
3. **Operational headroom.** Talos's `etcd snapshot` + `upgrade --stage` +
`apply-mode auto` reduce solo-founder ops burden. k3s is simpler to
learn but more error-prone to operate at HA.
### Acknowledged tradeoffs
- **Learning curve.** The Talos mental model (`machine config` + `talosctl`)
replaces SSH + systemd. Expect a one-week ramp for comfortable daily ops.
- **No shell debugging.** When something breaks on a node, you cannot `ssh`
in. Mitigation: `talosctl logs/support` produces a support bundle
equivalent to a sosreport.
- **Hardware/module surprises.** Talos ships a curated kernel. Anything
beyond virtio + common NIC drivers needs a system extension. On Proxmox
VMs this is **not expected to be a problem** — virtio is the path.
- **Tailscale via system extension.** Adds one config dimension per node.
Mitigated by Pattern C (above), which removes Tailscale from cluster
nodes entirely.
### What we keep from the k3s mental model
- **Single binary on each node** semantics (Talos is conceptually similar).
- **`kubectl` workflow unchanged.** Talos exposes a standard Kubernetes
API. `kubectl`, `helm`, `kustomize` all work as-is.
- **Storage CSI choices** (`local-fast`, `nfs-hdd`, `nfs-ssd`) are
distro-independent.
---
## 5. What we are NOT deciding here
| Topic | Deferred to |
|--------|-------------|
| ETL tooling (GDAL/PostGIS/xarray/Dask) | Future session — affects StorageClass RWX/RWO design |
| HPC scheduler (Jobs/Argo/Volcano) | Future session — affects taint/label strategy |
| Per-tenant vcluster policy templates | Future session, post-bootstrap |
| Solar-aware scale-out hosts | Future capacity planning session |
| Container network plugin (CNI) details | Will be specified in ARCHITECTURE.md §4 — recommendation is Cilium (supports NetworkPolicy, BPF, and encrypted node-to-node traffic for ITAR tenants) |
---
## 6. Next step
Proceed to [`ARCHITECTURE.md`](ARCHITECTURE.md) for the control-plane
design, network topology, identity flow, and bootstrap procedure.
+44
View File
@@ -0,0 +1,44 @@
# Kubernetes Architecture & Build Plan
> **Status:** Draft for review. Companion to [`../proxmox/K8S.md`](../proxmox/K8S.md)
> (which captured the storage/host analysis from the Proxmox audit).
> This directory takes the next step: **which distro, how to build it,
> how to operate it.**
**Last updated:** 2026-07-28
## Documents in this directory
| Document | Purpose |
|----------|---------|
| [`DISTRO-DECISION.md`](DISTRO-DECISION.md) | Talos vs k3s analysis. Recommendation: **Talos**, with rationale grounded in the ITAR/classified requirement. |
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Target architecture: control plane, network, identity, storage, tenant isolation. Mermaid diagrams included. |
## TL;DR
| Decision | Recommendation | Why |
|----------|----------------|-----|
| **Distro** | **k3s** (deployed) / **Talos** (for future ITAR) | k3s chosen for the regular R&D cluster now live on cnode1/2/3. Talos is the recommendation for when the ITAR/classified cluster comes online. |
| **Runtime** | **containerd** | Talos/k3s default. |
| **Cnode count** | **3** (deployed) | Standard HA. Tolerates 1 failure. |
| **Admin access** | **Tailscale (all nodes joined)** | Currently all cnodes are on Tailscale directly. For ITAR cluster, move to subnet-router pattern. |
| **Cluster network** | **Tailscale-only IPs** | All node-ip, advertise-address, TLS-SANs are 100.x Tailscale IPs. Zero LAN IPs in cluster state. |
| **Identity** | **OIDC to Keycloak** on Cloudron (production) | Future work. |
| **Multi-tenancy** | **vcluster** (per tenant) | Future work. |
| **Local registry** | **Harbor on D3 SSD** (tsys5, 445 GB free) | Future work. |
| **Storage classes** | `local-fast`, `nfs-hdd`, `nfs-ssd` | Per [`../proxmox/K8S.md`](../proxmox/K8S.md) §6. Future work. |
## What this directory does NOT cover (deferred)
- ETL tooling choice (GDAL/PostGIS/xarray/Dask) — affects RWX vs RWO design.
- HPC job scheduler (Jobs / Argo Workflows / Volcano) — affects taint/label strategy.
- vcluster per-tenant policy templates.
- Solar-aware scale-out (PowerEdge 19xx/2950 hosts) — capacity planning only.
These are tracked as future session work in [`../../STATUS.md`](../../STATUS.md).
## Open question for the user
1. **Cnode count: confirm 3 vs 5.** Recommendation is 3 (rationale in
[`ARCHITECTURE.md`](ARCHITECTURE.md) §2). If your ITAR counsel requires
2-failure tolerance on the control plane, keep 5.
+254
View File
@@ -0,0 +1,254 @@
# Fresh Fleet Audit — 2026-07-28
> **Supersedes placement data in [`PROJECT.md`](PROJECT.md) §4-§8.**
> The tables in PROJECT.md reflect the 2026-07-27 audit; VMs have since been
> migrated via PDM. This file is the current ground truth.
**Audit time:** 2026-07-28 19:55 CDT
**Method:** `qm list` + `qm config` on all 7 reachable hosts
**Hosts audited:** pfv-tsys1, pfv-tsys3, pfv-tsys4, pfv-tsys5, pfv-tsys6, pfv-tsys7, pfv-tsys9
**Hosts offline:** pfv-tsys2 (Win10, pending rebuild), pfv-tsys8 (offline 5+ days)
---
## 1. Host Summary
| Host | CPU | Threads | RAM (GB) | Local Disk | Role | Tuning |
|------|-----|---------|----------|-----------|------|--------|
| pfv-tsys1 | i7-4770 Haswell | 8 | 31 | HDD 932 GB | Infrastructure | Done |
| pfv-tsys3 | Xeon E3-1535M v5 Skylake | 8 | 31 | **NVMe 477 GB** | Kubernetes | Done |
| pfv-tsys4 | Xeon E3-1246 v3 Haswell | 8 | **15** | 6 disks (HDD+SSD) | Storage (NFS+PBS) | Blocked (NIC+RAM) |
| pfv-tsys5 | Xeon E5620 Westmere | 8 | **94** | 6 disks (HDD+SSD) | Storage+Preprod | Blocked (cable) |
| pfv-tsys6 | 2x Xeon E5530 Nehalem | 16 | 127 | HDD (USB 2.0!) | Kubernetes | Done |
| pfv-tsys7 | 2x Xeon E5-2630 v2 Ivy Bridge | 24 | 191 | HDD (USB 2.0!) | Kubernetes | Done |
| pfv-tsys9 | i5-10500 Comet Lake | 12 | 24 | SSD 250 GB | Infrastructure | Done |
**Changes since last audit:**
- tsys4 RAM still 15 GB (Friday 64 GB upgrade pending)
- tsys5 RAM is 94 GB (was documented as 96 GB)
- tsys6 RAM is 127 GB (was documented as 128 GB)
- tsys7 RAM is 191 GB (was documented as 192 GB)
---
## 2. VM Fleet Inventory (Running VMs Only)
### tsys1 — 11 running VMs (Infrastructure)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 100 | pfv-bms (HomeAssistant) | 2×1 | 4096 | 32 GB | D2 | tsys4 |
| 101 | tsys-ca | 2×1 | 2048 | 32 GB | D2 | tsys4 |
| 103 | **pfv-netinfra-01** | 2×1 | 2048 | 32 GB | D5 | tsys4 |
| 104 | tsys-librenms | 2×1 | 2048 | 50 GB | D2 | tsys4 |
| 105 | tsys-proxmox-datacenter | 2×1 | 2048 | 32 GB | D2 | tsys4 |
| 106 | **pfv-k8s-cnode3** | 2×1 | 4096 | 32 GB | **S3** | **tsys5** |
| 108 | **tsys-ucs-01** | 2×2 | 8000 | 32 GB | D2 | tsys4 |
| 109 | tailscale-router | 2×1 | 2048 | 25 GB | D2 | tsys4 |
| 114 | kali-tsys | 2×1 | 2048 | 32 GB | D2 | tsys4 |
| 117 | tsys-secure-workbench | 2×1 | 4000 | 32 GB | D2 | tsys4 |
| 102 | pfv-k8s-wnode-tsys1 | 4×1 | 4096 | 32 GB | S2 | tsys5 — **STOPPED** |
### tsys3 — 1 running VM (Kubernetes)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 313 | **pfv-k8s-wnode-tsys3** | 8×1 | 28000 | 32 GB | D5 | tsys4 |
### tsys4 — 1 running VM (Storage)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 400 | pfv-proxmox-backup-server | 2×1 | 2048 | 32 GB | local-lvm | LOCAL |
### tsys5 — 16 running VMs (Storage + Preprod)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 509 | **pfv-k8s-wnode-tsys5** | 2×4 | 32000 | 32 GB | D2 | tsys4 |
| 5101 | sectestbed-siem | 2×2 | 10000 | 132 GB | local-nonprod | LOCAL |
| 5105 | sectestbed-awx | 2×2 | 4096 | 288 GB | local-nonprod | LOCAL |
| 5106 | sectestbed-k8s-cnode | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5107 | sectestbed-k8s-wnode | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5108 | sectestbed-librenms | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5109 | sectestbed-netinfra | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5111 | ultix-streaming | 2×2 | 9000 | 288 GB | T5-SSD | tsys5 (SSD) |
| 5112 | ultix-offstage | 2×2 | 6000 | 288 GB | local-lvm | LOCAL |
| 6000 | sectestbed-sandbox | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51010 | sectestbed-tctc | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51011 | sectestbed-cloudron | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51012 | sectestbed-hfnoc | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51013 | sectestbed-rancherplatform | 2×2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 53100 | tsys-preprod-awx | 2×2 | 9000 | 160 GB | local-nonprod | LOCAL |
| 53101 | tsys-preprod-siem | 2×2 | 12000 | 32 GB | local-nonprod | LOCAL |
| 53102 | tsys-preprod-rancherplatform | 2×2 | 8000 | 32 GB | local-nonprod | LOCAL |
### tsys6 — 3 running VMs (Kubernetes)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 100 | **pfv-k8s-wnode-tsys6** | 2×2 | 32000 | 32 GB | D5 | tsys4 |
| 600 | tsys-awx | 2×2 | 12000 | 32 GB | D2 | tsys4 |
| 601 | pfv-k8s-cnode4 | 4×1 | 4096 | 32 GB | D2 | tsys4 |
### tsys7 — 6 running VMs (Kubernetes)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 701 | **pfv-k8s-wnode-tsys7** | 4×1 | 32000 | 32 GB | D5 | tsys4 |
| 702 | hfnoc-uisp | 2×2 | 8000 | 100 GB | D2 | tsys4 |
| 703 | rr-middleware | 2×1 | 2048 | 32 GB | D2 | tsys4 |
| 704 | TCTC | 4×1 | 6000 | 32 GB | D2 | tsys4 |
| 705 | **pfv-k8s-cnode2** | 4×1 | 4096 | 32 GB | D2 | tsys4 |
| 706 | pfv-k8s-cnode5 | 4×1 | 4096 | 32 GB | **S2** | **tsys5** |
### tsys9 — 6 running VMs (Infrastructure)
| VMID | Name | Cores×Sockets | RAM (MB) | Disk | Storage | NFS Server |
|------|------|--------------|----------|------|---------|-----------|
| 901 | tsys-siem | 2×1 | 8000 | 132 GB | D2 | tsys4 |
| 902 | **tsys-ucs-02** | 2×2 | 8000 | 50 GB | D5 | tsys4 |
| 903 | kali-rd | 2×1 | 2048 | 32 GB | D5 | tsys4 |
| 904 | **pfv-netinfra-02** | 2×1 | 4000 | 32 GB | D2 | tsys4 |
| 905 | **pfv-k8s-wnode-tsys9** | 4×1 | 4096 | 32 GB | **S2** | **tsys5** |
| 906 | **pfv-k8s-cnode1** | 2×1 | 4096 | 32 GB | D5 | tsys4 |
---
## 3. Kubernetes Node Placement
### 3.1 Active k3s cluster (cnode1/2/3 — deployed this session)
| Cnode | VMID | Host | Disk | NFS Server | Quorum risk |
|-------|------|------|------|-----------|-------------|
| cnode1 | **906** | tsys9 | D5 | **tsys4** | **HIGH** |
| cnode2 | **705** | tsys7 | D2 | **tsys4** | **HIGH** |
| cnode3 | 106 | tsys1 | S3 | tsys5 | OK |
**CRITICAL: 2 of 3 active cnodes on tsys4.** tsys4 failure = cnode1 + cnode2 die = 1 of 3 = **QUORUM LOST**.
**Fix needed:** Move cnode1 or cnode2 to tsys5 storage (S2 or S3). One migration via PDM "Storage Migrate" solves this.
### 3.2 Inactive cnodes (cnode4/5 — exist but not in k3s cluster)
| Cnode | VMID | Host | Disk | NFS Server |
|-------|------|------|------|-----------|
| cnode4 | 601 | tsys6 | D2 | tsys4 |
| cnode5 | 706 | tsys7 | S2 | tsys5 |
### 3.3 Worker nodes (wnodes)
| Wnode | VMID | Host | Disk | NFS Server | RAM | Status |
|-------|------|------|------|-----------|-----|--------|
| wnode-tsys1 | 102 | tsys1 | S2 | tsys5 | 4 GB | STOPPED |
| wnode-tsys3 | 313 | tsys3 | D5 | tsys4 | 28 GB | Running |
| wnode-tsys5 | 509 | tsys5 | D2 | tsys4 | 32 GB | Running |
| wnode-tsys6 | 100 | tsys6 | D5 | tsys4 | 32 GB | Running |
| wnode-tsys7 | 701 | tsys7 | D5 | tsys4 | 32 GB | Running |
| wnode-tsys9 | 905 | tsys9 | S2 | tsys5 | 4 GB | Running |
**Storage: 4 wnodes on tsys4, 2 on tsys5.** One wnode per host achieved.
---
## 4. Critical HA Pairs — Storage Redundancy
### 4.1 netinfra pair — FAILED (both on tsys4)
| Role | VMID | Host | Disk | NFS Server |
|------|------|------|------|-----------|
| netinfra-01 | 103 | tsys1 | D5 | **tsys4** |
| netinfra-02 | 904 | tsys9 | D2 | **tsys4** |
**tsys4 failure = DNS/DHCP/NTP goes fully dark.**
**Fix:** Migrate netinfra-02 (VMID 904) from D2 (tsys4) to S3 (tsys5) via PDM.
### 4.2 UCS pair — FAILED (both on tsys4)
| Role | VMID | Host | Disk | NFS Server |
|------|------|------|------|-----------|
| ucs-01 | 108 | tsys1 | D2 | **tsys4** |
| ucs-02 | 902 | tsys9 | D5 | **tsys4** |
**tsys4 failure = LDAP/AD goes fully dark.**
**Fix:** Migrate ucs-02 (VMID 902) from D5 (tsys4) to S2 (tsys5) via PDM.
### 4.3 Corrected placement (after migration)
| VM | Host | Disk | NFS Server | Failure survival |
|----|------|------|-----------|-----------------|
| netinfra-01 | tsys1 | D5 | tsys4 | tsys4 dies → netinfra-02 alive on tsys5 |
| netinfra-02 | tsys9 | **S3** | **tsys5** | tsys5 dies → netinfra-01 alive on tsys4 |
| ucs-01 | tsys1 | D2 | tsys4 | tsys4 dies → ucs-02 alive on tsys5 |
| ucs-02 | tsys9 | **S2** | **tsys5** | tsys5 dies → ucs-01 alive on tsys4 |
---
## 5. Storage Concentration
| Storage target | # running VMs | % of fleet |
|---------------|--------------|------------|
| D2 (tsys4 WDC Red 3TB HDD) | 16 | 36% |
| D5 (tsys4 Hitachi 2TB HDD) | 10 | 23% |
| local-nonprod (tsys5 local HDD) | 12 | 27% |
| S2 (tsys5 Seagate 1TB HDD) | 3 | 7% |
| S3 (tsys5 Seagate 1TB HDD) | 1 | 2% |
| T5-SSD (tsys5 Samsung SSD) | 1 | 2% |
| local-lvm (various hosts) | 2 | 5% |
**26 of 44 running VMs (59%) store their disks on tsys4 NFS exports.**
(Was 68% in the previous audit — improving but still concentrated.)
---
## 6. Pre-k8s Buildout Action Items
These must be done before or during k8s worker node bringup:
### 6.1 CRITICAL: Migrate HA pairs to separate storage (PDM, 10 min)
1. `netinfra-02` (VMID 904): D2 → S3 (tsys4 → tsys5)
2. `ucs-02` (VMID 902): D5 → S2 (tsys4 → tsys5)
These are PDM "Storage Migrate" operations — no VM rebuild needed.
### 6.2 CRITICAL: Fix active cnode quorum (PDM, 5 min)
Move one active cnode from tsys4 to tsys5 storage:
- Best candidate: cnode1 (VMID 906) D5 → S2 (tsys4 → tsys5)
- Result: cnode1 on tsys5, cnode2 on tsys4, cnode3 on tsys5
- tsys4 failure = cnode2 dies only = 2 of 3 = **quorum OK**
### 6.3 Join workers to k3s cluster
Current wnodes exist as VMs but are not joined to the k3s cluster. Need to:
1. Install k3s agent on each wnode (using join token from cnode1)
2. Configure `--node-ip=<tailscale-ip>` on each
3. Label/taint per workload role
### 6.4 Friday hardware work (still pending)
- tsys4: PCIe NIC + 64 GB RAM (currently 15 GB)
- tsys5: 2nd ethernet cable + NVMe + D3 SSD relocation
- tsys2: Rebuild from Win10 to Proxmox
---
## 7. Changes Since Previous Audit (2026-07-27)
| What | Before | After |
|------|--------|-------|
| cnode1 VMID | 107 (tsys1, D5) | **906** (tsys9, D5) |
| cnode2 VMID | 603 (tsys6, D2) | **705** (tsys7, D2) |
| cnode3 storage | D2 (tsys4) | **S3** (tsys5) |
| cnode5 VMID | 602 (tsys6) | **706** (tsys7, S2 tsys5) |
| wnode-tsys1 | not listed | VMID 102 (S2 tsys5, stopped) |
| wnode-tsys3 RAM | 20 GB | **28 GB** |
| tsys-awx (600) | STOPPED | **Running** |
| DellOpenManageEnterprise (500) | not listed | VMID 500 (D7, stopped) |
| sectestbed-librenms (5108) | not listed | Running |
| k3s cluster | not deployed | **3-node HA live** (cnode1/2/3) |
| Console management | manual screen | **ser2net+conman on tsys4** |
| PDU management | manual | **powerman on tsys1** |
+485
View File
@@ -0,0 +1,485 @@
# K8S.md -- Kubernetes Architecture Deep-Dive
**Date:** 2026-07-27
**Purpose:** Detailed kubernetes architecture plan for the pfv-k8s cluster.
Companion to [`PROJECT.md`](PROJECT.md) (which has the fleet-wide assessment).
**Status:** For discussion in a future session. No changes made.
---
## Table of Contents
1. [Workload Profile](#1-workload-profile)
2. [Current State](#2-current-state)
3. [Target Architecture](#3-target-architecture)
4. [Control Plane (Cnodes)](#4-control-plane-cnodes)
5. [Worker Nodes (Wnodes)](#5-worker-nodes-wnodes)
6. [Storage Class Design](#6-storage-class-design)
7. [ETL/HPC Considerations](#7-etlhpc-considerations)
8. [Migration Plan](#8-migration-plan)
---
## 1. Workload Profile
This cluster runs **R&D and RackRental (containerlab) workloads** via
Kubernetes. Production (Gitea, RustFS, Redmine, websites) lives on a VPS in
Reston, VA running Cloudron.
**Workload types expected:**
| Type | Description | Storage need | RAM need | Examples |
|------|------------|-------------|----------|---------|
| **ETL (weather/GIS)** | Batch processing of large geospatial datasets. Sequential reads, transform, sequential writes. | High capacity (100s of GB), moderate IOPS | Medium (8-32 GB per job) | GRIB/NetCDF processing, raster reprojection |
| **HPC (hardware startup)** | Compute-intensive simulations, firmware build pipelines, hardware-in-the-loop testing. | Low capacity, moderate IOPS | High (32-128 GB per job) | RTL simulation, PCB thermal analysis |
| **RackRental (containerlab)** | Rapid deployment/teardown of network lab topologies. Many containers, short-lived. | Low capacity, high IOPS (container image pulls) | Low-Medium (4-16 GB) | Network topology testing, protocol validation |
**Key storage insight:** ETL workloads need bulk capacity (NFS-HDD is fine --
sequential I/O). HPC and containerlab need low-latency random I/O (local
SSD/NVMe is essential). The tiered StorageClass design (section 6) serves both.
---
## 2. Current State
### 2.1 pfv-k8s nodes and their storage
| Node | Type | Host | Storage | Disk type | Status |
|------|------|------|---------|-----------|--------|
| cnode1 (107) | control | tsys1 | D5 (tsys4) | NFS-HDD | running |
| cnode2 (603) | control | tsys6 | D2 (tsys4) | NFS-HDD | running |
| cnode3 (106) | control | tsys1 | D2 (tsys4) | NFS-HDD | running |
| cnode4 (601) | control | tsys6 | D2 (tsys4) | NFS-HDD | running |
| cnode5 (602) | control | tsys6 | D5 (tsys4) | NFS-HDD | running |
| wnode-tsys3 (313) | worker | tsys3 | D5 (tsys4) | NFS-HDD | running |
| wnode-tsys5 (509) | worker | tsys5 | D2 (tsys4) | NFS-HDD | running |
| wnode-tsys6 (100) | worker | tsys6 | D5 (tsys4) | NFS-HDD | **STOPPED** |
| wnode-tsys7 (701) | worker | tsys7 | D5 (tsys4) | NFS-HDD | running |
| wnode-tsys9 (905) | worker | tsys9 | S3 (tsys5) | NFS-HDD | running |
### 2.2 Problems
1. **100% of cnodes on tsys4 NFS.** D2 disk failure loses 3 of 5 cnodes =
etcd quorum lost.
2. **90% of all k8s nodes on tsys4 NFS.** tsys4 failure kills the cluster.
3. **Zero nodes use SSD or NVMe.** All on NFS-over-HDD.
4. **Zero nodes use local-lvm.** tsys3/6/7/9 all have empty local storage
(349 GB / 1.7 TB / 1.7 TB / 136 GB SSD respectively).
5. **wnode-tsys6 is stopped.** Reduces cluster capacity.
6. **3 cnodes on tsys6** -- should be on lighter hosts to free tsys6 for workers.
---
## 3. Target Architecture
### 3.1 Design principles
1. **Cnodes on lightweight hosts** (tsys1, tsys9, tsys3) -- frees tsys6/7 for
heavy workers.
2. **Cnode storage split across tsys4 and tsys5** -- etcd survives either
storage server failing.
3. **Wnode boot disks on local storage** -- eliminates NFS latency for
container runtime and kubelet.
4. **Wnode data disks on NFS-HDD** -- bulk capacity for ETL/weather/GIS.
5. **tsys5 NVMe dedicated to wnode-tsys5** -- fastest tier for HPC jobs.
6. **One wnode per hypervisor host** -- maximize total cluster capacity.
### 3.2 Target node-host-storage matrix
| Node | Type | Host | Boot disk | Data disk | Disk type |
|------|------|------|-----------|-----------|-----------|
| cnode1 | control | tsys1 | D5 (tsys4) | -- | NFS-HDD |
| cnode2 | control | tsys9 | D2 (tsys4) | -- | NFS-HDD |
| cnode3 | control | tsys1 | S2 (tsys5) | -- | NFS-HDD |
| cnode4 | control | tsys9 | D5 (tsys4) | -- | NFS-HDD |
| cnode5 | control | tsys3 | S3 (tsys5) | -- | NFS-HDD |
| wnode-tsys1 | worker | tsys1 | D5 (tsys4) | -- | NFS-HDD (small) |
| wnode-tsys2 | worker | tsys2 | **NVMe (960 PRO 512GB)** | **SATA SSD (850 EVO 1TB)** | **NVMe + SSD -- no NFS needed** |
| wnode-tsys3 | worker | tsys3 | **local-lvm (NVMe PM961)** | S3 (NFS) | **LOCAL-NVMe** |
| wnode-tsys5 | worker | tsys5 | **NVMe (local, Friday)** | local-nonprod (HDD) | **NVMe** |
| wnode-tsys6 | worker | tsys6 | D2 (tsys4 NFS) | -- | NFS-HDD (local-lvm is USB 2.0 -- do not use) |
| wnode-tsys7 | worker | tsys7 | D5 (tsys4 NFS) | -- | NFS-HDD (local-lvm is USB 2.0 -- do not use) |
| wnode-tsys9 | worker | tsys9 | **local-lvm (SSD)** | S2 (NFS) | **LOCAL-SSD** |
### 3.3 Storage server distribution after changes
| Storage server | cnodes | wnodes (boot) | wnodes (data) |
|---------------|--------|---------------|---------------|
| tsys4 (D2) | cnode2 | wnode-tsys6 | wnode-tsys7 |
| tsys4 (D5) | cnode1, cnode4 | wnode-tsys1 | -- |
| tsys5 (S2) | cnode3 | wnode-tsys9 | -- |
| tsys5 (S3) | cnode5 | -- | wnode-tsys3 |
**Note:** wnode-tsys2 needs no NFS (1.5 TB local SSD). wnode-tsys6/7 stay on
NFS by design -- their local-lvm is USB 2.0 portable HDD (~30 MB/s), slower
than NFS-HDD, and the user has chosen not to install internal drives.
**No single disk or server is a quorum-losing failure point.**
---
## 4. Control Plane (Cnodes)
### 4.1 Cnode sizing
Each cnode: 4 cores, 4 GB RAM, 32 GB disk. This is sufficient for etcd +
kubernetes control plane components (API server, scheduler, controller-manager).
### 4.2 Cnode host placement rationale
| Host | cnodes | RAM for cnodes | Total host RAM | Remaining for other VMs |
|------|--------|---------------|---------------|------------------------|
| tsys1 | 2 (cnode1, cnode3) | 8 GB | 32 GB | ~24 GB (but 11 infra VMs consume most) |
| tsys9 | 2 (cnode2, cnode4) | 8 GB | 24 GB | ~16 GB (4 infra VMs + 1 wnode) |
| tsys3 | 1 (cnode5) | 4 GB | 32 GB | ~28 GB (1 wnode at 20 GB = 8 GB headroom) |
**tsys6 and tsys7 have ZERO cnodes** -- fully dedicated to heavy worker nodes.
### 4.3 Cnode storage placement rationale
The 5 cnodes are split 3-on-tsys4 / 2-on-tsys5:
| Disk | cnodes | Rationale |
|------|--------|-----------|
| D5 (tsys4 HDD) | cnode1, cnode4 | Spread load across 2 disks on tsys4 |
| D2 (tsys4 HDD) | cnode2 | Only 1 cnode on D2 (was 3 -- reduces blast radius) |
| S2 (tsys5 HDD) | cnode3 | tsys5 storage for quorum diversity |
| S3 (tsys5 HDD) | cnode5 | tsys5 storage, different disk than S2 |
**If D2 fails:** cnode2 dies. 4 of 5 survive. Quorum OK.
**If D5 fails:** cnode1 + cnode4 die. 3 of 5 survive. Quorum OK.
**If tsys4 fails:** cnode1, cnode2, cnode4 die. cnode3 + cnode5 survive on
tsys5. **Only 2 of 5 -- QUORUM LOST.**
Wait -- that is a problem. If tsys4 goes completely offline, we lose 3
cnodes and only have 2 on tsys5. That loses quorum (need 3).
**Revision needed:** Move 1 more cnode to tsys5 storage. Target: 2 on tsys4,
3 on tsys5. But that means tsys5 failure (3 cnodes die) leaves only 2 on
tsys4. Same problem inverted.
The fundamental issue: with 5 cnodes and 2 storage servers, the best split is
3/2. The server holding 3 cnodes is a quorum-loss risk if it fails. The server
holding 2 cnodes is safe (3 survive).
**Proper solution: 3 cnodes on the "less likely to fail" server, 2 on the
other.** After Friday's hardware work:
- tsys4 will have a new PCIe NIC + 64 GB RAM -- more reliable
- tsys5 will have bond0 fixed + NVMe -- more reliable
Either way, 3/2 split means one server failure could lose quorum. **To truly
solve this, use a 3rd storage target.** Options:
- Use tsys9 local SSD for 1 cnode (breaks the 2-server model, adds a 3rd
independent failure domain)
- Use local-lvm on the cnode's own host (etcd data is local to the VM's host,
no NFS dependency at all)
**Best option: put cnode boot disks on local-lvm where available.** This
eliminates NFS entirely for the control plane. Each cnode's etcd data lives on
its own host's local disk -- no shared dependency.
| cnode | Host | **Recommended storage** | Type |
|-------|------|------------------------|------|
| cnode1 | tsys1 | **local-lvm** (if space) or D5 (tsys4) | LOCAL-HDD or NFS-HDD |
| cnode2 | tsys9 | **local-lvm (SSD)** | **LOCAL-SSD** |
| cnode3 | tsys1 | **S2 (tsys5)** | NFS-HDD |
| cnode4 | tsys9 | **local-lvm (SSD)** | **LOCAL-SSD** |
| cnode5 | tsys3 | **local-lvm** | LOCAL-HDD |
With this layout, a tsys4 failure takes down 0 cnodes. A tsys5 failure takes
down 1 (cnode3). A host failure takes down at most 2 cnodes. Quorum always
survives.
**This is the recommended approach.** Local storage for cnodes wherever
possible. NFS only as fallback.
### 4.4 etcd performance on local vs NFS
| Storage | Typical fsync latency | etcd commit latency | Impact |
|---------|----------------------|--------------------|--------|
| NFS-HDD (via USB dongle on tsys4) | 5-15 ms | 10-30 ms | Slow API responses, sluggish pod scheduling |
| NFS-HDD (via PCIe NIC, post-Friday) | 2-8 ms | 5-15 ms | Better but still network-bound |
| Local HDD (tsys1/3/6/7 local-lvm) | 1-5 ms | 3-10 ms | No network hop, moderate improvement |
| Local SSD (tsys9 PNY CS900) | 0.1-0.5 ms | 0.5-2 ms | **10-30x faster than NFS-HDD** |
| NVMe (tsys5, Friday) | 0.02-0.1 ms | 0.1-0.5 ms | **100x faster than NFS-HDD** |
etcd is the heartbeat of the kubernetes control plane. Every API call, every
pod schedule, every controller reconciliation involves an etcd write. Cutting
etcd commit latency from 15 ms to 1 ms makes the entire cluster feel 15x more
responsive. **This is the single highest-impact change for k8s performance.**
---
## 5. Worker Nodes (Wnodes)
### 5.1 One wnode per hypervisor host
| Host | wnode | Boot disk | Data disk | Total RAM | wnode RAM | Role |
|------|-------|-----------|-----------|-----------|-----------|------|
| tsys1 | wnode-tsys1 | D5 (tsys4 NFS) | -- | 32 GB | 4-8 GB | Small worker, infra co-tenant |
| tsys2 | wnode-tsys2 | **NVMe (960 PRO 512GB)** | **SATA SSD (850 EVO 1TB)** | 32 GB | 16-24 GB | **Best storage of any worker -- 1.5TB local SSD, no NFS needed** |
| tsys3 | wnode-tsys3 | **local-lvm (349 GB)** | S3 (NFS) | 32 GB | 20 GB | General worker |
| tsys5 | wnode-tsys5 | **NVMe (local)** | local-nonprod (HDD) | 96 GB | 32-64 GB | **HPC/ETL powerhouse** |
| tsys6 | wnode-tsys6 | D2 (tsys4 NFS) | -- | 128 GB | 64-96 GB | **Heavy worker, max RAM.** local-lvm is USB 2.0 -- stays on NFS |
| tsys7 | wnode-tsys7 | D5 (tsys4 NFS) | -- | 192 GB | 96-128 GB | **Heavy worker, max RAM.** local-lvm is USB 2.0 -- stays on NFS |
| tsys9 | wnode-tsys9 | **local-lvm SSD (136 GB)** | S2 (NFS) | 24 GB | 4-8 GB | Small worker, SSD boot |
### 5.2 Why boot disks on local-lvm
Current: all wnodes boot from NFS. Every container image pull, every kubelet
log write, every ephemeral volume traverses the NFS network path.
With local-lvm boot disks:
- **Container image pulls** write to local disk (100-150 MB/s HDD, no network
hop) instead of NFS-HDD (80-120 MB/s with network latency)
- **kubelet logs** stay local (no NFS writes for log rotation)
- **ephemeral storage** (emptyDir volumes) uses local disk by default
- **NFS server failure does not kill the wnode** -- the VM stays running, only
the data disk (if mounted) goes away
### 5.3 Wnode sizing guidance
| Host | Recommended wnode config | Rationale |
|------|------------------------|-----------|
| tsys7 (192 GB) | 8-12 cores, 96-128 GB RAM, NFS boot | Largest host -- run the heaviest ETL/HPC jobs here. local-lvm is USB 2.0 |
| tsys6 (128 GB) | 8 cores, 64-96 GB RAM, NFS boot | Second-largest -- parallel heavy jobs. local-lvm is USB 2.0 |
| tsys5 (96 GB + NVMe) | 4 cores, 32-64 GB RAM, NVMe boot + HDD data | NVMe makes this fastest for I/O-bound HPC |
| tsys3 (32 GB) | 4 cores, 20 GB RAM, local-lvm boot | General-purpose worker |
| tsys2 (32 GB, NVMe+SSD, incoming) | 4 cores, 16-24 GB RAM, **NVMe boot + SSD data** | **Fastest storage worker** -- HPC with I/O bounds |
| tsys1 (32 GB) | 2 cores, 4-8 GB RAM | Small worker, don't starve infra VMs |
| tsys9 (24 GB) | 2-4 cores, 4-8 GB RAM | Small worker, SSD boot is the advantage |
### 5.4 Tainting and labeling strategy
Label wnodes by capability so the k8s scheduler can target them:
```yaml
# Heavy RAM hosts (ETL/HPC)
wnode-tsys6: workload=heavy, ram=128g
wnode-tsys7: workload=heavy, ram=192g
# NVMe host (I/O-intensive HPC)
wnode-tsys5: workload=hpc, storage=nvme
# SSD boot host (low-latency)
wnode-tsys9: workload=light, storage=ssd
# General workers
wnode-tsys3: workload=general
wnode-tsys2: workload=storage-fast, storage=nvme
wnode-tsys1: workload=light
```
Then use nodeSelector or nodeAffinity in job specs:
```yaml
# Weather/GIS ETL job -- needs lots of RAM
spec:
nodeSelector:
workload: heavy
# Firmware build -- needs fast storage
spec:
nodeSelector:
storage: nvme
```
---
## 6. Storage Class Design
### 6.1 Proposed StorageClasses
| StorageClass | Provisioner | Where | Speed | Use case |
|-------------|------------|-------|-------|----------|
| `local-fast` | local-path (k8s) | wnode local-lvm / NVMe | 100-3500 MB/s | Container runtime, scratch, databases |
| `nfs-hdd` | nfs-subdir-external-provisioner | tsys4 D2/D5, tsys5 S1-S4 | 80-120 MB/s | Bulk data, weather/GIS datasets |
| `nfs-ssd` | nfs-subdir-external-provisioner | tsys4 D3, tsys5 T5-SSD | 200-400 MB/s | Latency-sensitive persistent data |
### 6.2 How this maps to wnode disk topology
Each wnode has:
- **Disk 1 (boot/OS):** local-lvm or NVMe. Contains the OS, kubelet, container
runtime. k8s `local-fast` StorageClass provisioner points here.
- **Disk 2 (bulk data, optional):** NFS mount. Mounted inside the VM as a
second block device or filesystem. k8s `nfs-hdd` provisioner points here.
Inside k8s, pods request storage via PVC:
```yaml
# ETL job: needs bulk storage for weather data
apiVersion: v1
kind: PersistentVolumeClaim
spec:
storageClassName: nfs-hdd
accessModes: [ReadWriteMany] # NFS allows RWX
resources:
requests:
storage: 500Gi
# HPC job: needs fast scratch
spec:
storageClassName: local-fast
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 50Gi
```
### 6.3 NFS-SSD tier (D3 and T5-SSD -- both on tsys5 after Friday)
**Storage philosophy (user directive): NVMe/SSD is EXCLUSIVELY for k8s worker
scratch space, with the exception of ultix-streaming which stays on T5-SSD.
Spinning rust hosts all other infrastructure VMs** (UCS, netinfra, LibreNMS,
SIEM, etc.).
The SSD NFS exports:
- **D3 (tsys5 SAS, 445 GB free):** k8s scratch exclusively (etcd, container
cache, ephemeral volumes). Currently 99% empty.
- **T5-SSD (tsys5 SAS, 140 GB free after ultix-streaming):** ultix-streaming
occupies 83 GB. Remaining 140 GB available for k8s use.
**tsys5 is the fast-tier hub:** NVMe (local) + D3 SSD + T5-SSD all on one host.
This simplifies the StorageClass design -- latency-sensitive k8s PVCs target
tsys5 SSD exports, bulk PVCs target either server.
### 6.4 NFS data distribution across storage servers
To avoid re-creating the "everything on tsys4" problem, distribute NFS data
disks across both servers:
| wnode | Boot (local) | Bulk data (NFS) | NFS server |
|-------|-------------|-----------------|------------|
| wnode-tsys3 | local-lvm | S3 | tsys5 |
| wnode-tsys5 | NVMe | local-nonprod | local (no NFS) |
| wnode-tsys6 | D2 (tsys4 NFS) | -- | tsys4 |
| wnode-tsys7 | D5 (tsys4 NFS) | -- | tsys4 |
| wnode-tsys9 | local-lvm (SSD) | S2 | tsys5 |
This balances: 2 wnodes using tsys4 for bulk data, 2 using tsys5.
---
## 7. ETL/HPC Considerations
### 7.1 Weather/GIS ETL pipeline
Typical flow: download GRIB/NetCDF files -> process (reproject, aggregate) ->
store results.
| Stage | Storage class | Why |
|-------|-------------|-----|
| Download raw data | `nfs-hdd` | Large sequential writes. NFS-HDD handles this well. |
| Processing scratch | `local-fast` | Random access during transform. Local disk avoids NFS latency. |
| Store results | `nfs-hdd` | Large sequential writes. Persistent. |
**Recommendation:** Deploy a `local-fast` PV mount as `/scratch` on every
wnode. ETL jobs use `/scratch` for intermediate processing and write final
output to the NFS-mounted `/data`.
### 7.2 HPC workloads (hardware startup)
Use cases: RTL simulation, PCB thermal analysis, firmware build pipelines.
| Workload | Best wnode | Why |
|----------|-----------|-----|
| RTL simulation (CPU-bound, high RAM) | tsys7 (192 GB) | Most RAM, most cores (24t) |
| Firmware builds (I/O-bound, moderate RAM) | tsys5 (NVMe) | Fastest storage for compile I/O |
| Hardware-in-the-loop (latency-sensitive) | tsys9 (local SSD) | Lowest latency storage |
| Parallel batch jobs | tsys6 + tsys7 | Distribute across both heavy hosts |
### 7.3 RackRental/containerlab
Rapid container deployment. Key need: fast container image pulls.
This is where **local-lvm boot disks** shine. Currently, every container image
pull writes through NFS to a spinning disk -- slow. With local-lvm, images
cache on local disk (even HDD is 2-3x faster than NFS-HDD for random I/O).
On tsys9 (SSD) and tsys5 (NVMe), image pulls are near-instant.
### 7.4 Data locality for ETL
For weather/GIS data that is read repeatedly (e.g., climate reanalysis), cache
it on local-lvm of the heavy hosts:
```
tsys3 local-lvm (NVMe 349 GB): /data/cache/weather/ -- fastest cache tier
tsys5 NVMe (local): /data/cache/gis/ -- fastest cache tier
```
**Note:** tsys6/7 local-lvm is USB 2.0 portable HDD (~30 MB/s) -- cannot
be used for caching. Pre-populate weather/GIS data on D2/D5 (NFS) instead.
This avoids re-reading the same data from the same NFS export on every job
if the data is already cached in the page cache.
---
## 8. Migration Plan
**Key enabler:** The hosts are standalone Proxmox installs, but **Proxmox
Datacenter Manager (PDM)** manages them collectively and supports VM migration
between nodes. Storage migration can be done via the PDM/Proxmox UI rather
than manual disk copies -- the destination node just needs access to the target
storage (which all nodes have for NFS exports, and local storage can be
migrated through the UI's "Storage Migrate" function).
### 8.1 Phase 1: Friday (after hardware work)
After tsys5 cable + NVMe and tsys4 NIC + RAM:
1. **Format tsys5 NVMe** as local directory storage (e.g., `nvme-local`)
2. **Restart wnode-tsys6** (VM 100). Keep on NFS (D5). local-lvm is USB 2.0 --
do not use for VM storage. Recreate on D2 or D5 NFS.
3. **Move wnode-tsys9** (VM 905) disk from S3 (NFS) to local-lvm (SSD).
### 8.2 Phase 2: Cnode rebalance (maintenance window)
These changes require creating new VMs on target hosts and migrating disks.
Plan for a maintenance window with the k8s cluster briefly down.
1. Create cnode2 on tsys9 (local-lvm SSD if possible, or D2 NFS).
2. Create cnode4 on tsys9 (D5 NFS or local-lvm SSD).
3. Create cnode5 on tsys3 (S3 NFS or local-lvm).
4. Move cnode3 disk from D2 to S2 (tsys4 to tsys5).
5. Join new cnodes to etcd cluster, drain old cnode2/4/5, remove.
### 8.3 Phase 3: Wnode local storage migration (maintenance window)
1. Recreate wnode-tsys3 with boot disk on local-lvm (349 GB).
2. wnode-tsys6 stays on NFS (local-lvm is USB 2.0 HDD -- not suitable).
3. wnode-tsys7 stays on NFS (same reason).
4. Recreate wnode-tsys5 with boot disk on NVMe.
5. Add data disks (NFS) as second SCSI devices where applicable.
### 8.4 Phase 4: tsys2 integration (when rebuilt)
1. Install Proxmox on tsys2.
2. Run `scripts/check.sh` to inventory.
3. Run `scripts/apply-tunings.sh --apply`.
4. Create wnode-tsys2 with **boot disk on NVMe (960 PRO)** and **data disk on SATA SSD (850 EVO)**. No NFS needed -- 1.5 TB local SSD is the most local storage of any worker.
5. Join to k8s cluster.
### 8.5 Phase 5: Critical VM relocation
1. Move netinfra-02 (VM 904) from D2 to S3 (tsys5 HDD).
2. Move ucs-02 (VM 902) from D5 to S2 (tsys5 HDD).
3. (No change to T5-SSD -- ultix-streaming stays.)
---
## Open questions for next session
1. **Are the hosts a Proxmox cluster (pvecm) or standalone?** This determines
whether live migration is available (huge simplification) or we need manual
disk migration. Check `pvecm status` on each host.
2. **What k8s distribution is in use?** (k3s, kubeadm, RKE2?) This affects how
nodes are joined/drain and how StorageClasses are configured.
3. **Container runtime?** (containerd, cri-o?) Affects local storage layout.
4. **Is there a container image registry mirror in the cluster?** Or do all
pulls go to Docker Hub / external? A local registry on D3 SSD would speed
up all pulls.
5. **What specific ETL tools?** (GDAL, PostGIS, xarray, Dask?) This affects
whether jobs need shared (RWX) or exclusive (RWO) storage.
6. **HPC job scheduler?** (plain k8s Jobs, Argo Workflows, Volcano?) Affects
how we label and taint nodes.
+857
View File
@@ -0,0 +1,857 @@
# Proxmox Cluster Project Report
**Date:** 2026-07-27 (re-audited)
**Prepared by:** Performance Optimization Engagement
**Status:** Comprehensive fleet assessment with VM placement and redundancy analysis
**Data freshness:** All 7 hosts re-audited at 21:50 CDT 2026-07-27 via
`deploy-check.sh`. VM placements reflect live state after user's PDM
migrations. This is ground truth.
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Host Fleet](#2-host-fleet)
3. [Storage Architecture](#3-storage-architecture)
4. [VM Fleet Inventory](#4-vm-fleet-inventory)
5. [Kubernetes Node Distribution](#5-kubernetes-node-distribution)
6. [Storage Redundancy Analysis](#6-storage-redundancy-analysis)
7. [Local SSD/NVMe Opportunity](#7-local-ssdnvme-opportunity)
8. [Role Alignment Audit](#8-role-alignment-audit)
9. [Network Findings](#9-network-findings)
10. [Recommendations](#10-recommendations)
11. [Hardware End-of-Support Exposure](#11-hardware-end-of-support-exposure)
12. [Open Items](#12-open-items)
---
## 1. Executive Summary
The cluster consists of 7 active Proxmox hosts and 1 incoming (pfv-tsys2),
running 43 VMs across two NFS storage servers (tsys4, tsys5). Host-side
performance tunings are complete on 5 of 7 hosts. Two hosts (tsys4, tsys5)
are blocked on physical hardware work scheduled for Friday.
**Progress since initial audit:** The user has been actively rebalancing k8s
nodes via PDM. Storage distribution improved from 90%/10% (tsys4/tsys5) to
73%/27%. One cnode now uses tsys5 storage (cnode5 on S2). More migration
needed for etcd quorum survival.
The VM-layer assessment reveals:
| # | Finding | Severity | Status |
|---|---------|----------|--------|
| 1 | **4 of 5 cnodes still store disks on tsys4 NFS.** cnode5 moved to tsys5. Still need 1-2 more moves for quorum survival. | **CRITICAL** | Improving |
| 2 | **Both -01/-02 infrastructure pairs (netinfra, UCS) on tsys4 NFS only.** | **HIGH** | TODO today |
| 3 | **No k8s node uses SSD or NVMe yet.** tsys3 has 349 GB unused local NVMe; tsys9 has 136 GB local SSD. | **HIGH** | Deferred to k8s session |
| 4 | **D3 SSD (tsys4, USB) is 99% empty (445 GB free).** Moving to tsys5 SAS Friday. | **MEDIUM** | Friday |
---
## 2. Host Fleet
### 2.1 Inventory
| Host | Model | CPU (year) | Cores | RAM | Local Disk | Role (intended) | Tuning |
|------|-------|-----------|-------|-----|-----------|-----------------|--------|
| pfv-tsys1 | OptiPlex 9020 | i7-4770 Haswell (2013) | 4c/8t | 32 GB DDR3 | HDD (LVM-thin) | **Infrastructure** | Done |
| pfv-tsys2 | Precision 5520 | i7-7820HQ Kaby Lake (2017) | 4c/8t | 32 GB (max) | **NVMe 512GB + SATA SSD 1TB** | **Kubernetes** | Incoming (Win10) |
| pfv-tsys3 | Precision 7510 | Xeon E3-1535M v5 Skylake (2015) | 4c/8t | 32 GB DDR4 | HDD (LVM-thin) | **Kubernetes** | Done |
| pfv-tsys4 | Precision T1700 | Xeon E3-1246 v3 Haswell (2013) | 4c/8t | 16 GB DDR3 | 6 disks (HDD+SSD+SMR) | **Storage (NFS+PBS)** | Blocked (NIC+RAM) |
| pfv-tsys5 | Precision T7500 | Xeon E5620 Westmere (2010) | 4c/8t | 96 GB DDR3 | 6 disks (HDD+SSD) | **Storage (NFS+VMs)** | Blocked (cable) |
| pfv-tsys6 | PowerEdge R610 | 2x Xeon E5530 Nehalem (2009) | 8c/16t | 128 GB DDR3 | HDD (LVM-thin) | **Kubernetes** | Done |
| pfv-tsys7 | PowerEdge R620 | 2x Xeon E5-2630 v2 Ivy Bridge (2013) | 12c/24t | 192 GB DDR3 | HDD (LVM-thin) | **Kubernetes** | Done |
| pfv-tsys9 | OptiPlex 7080 | i5-10500 Comet Lake (2020) | 6c/12t | 24 GB DDR4 | **250 GB SSD** (PNY CS900) | **Infrastructure** | Done |
### 2.2 Role taxonomy (per user directive)
| Role | Hosts | Workload |
|------|-------|----------|
| **Infrastructure + k8s control** | tsys1, tsys9 | Infra VMs (netinfra, UCS, PBS, CA, HA) + pfv-k8s cnodes (control plane) + small wnodes |
| **Kubernetes workers** | tsys2, tsys3, tsys6, tsys7 | pfv-k8s wnodes (heavy workers) -- these hosts have the RAM (32-192 GB) for ETL/HPC |
| **Storage** | tsys4, tsys5 | NFS server + PBS backup target. tsys5 also runs sectestbed/preprod VMs |
**Design rationale:** cnodes (control plane) are lightweight (4 cores, 4 GB
RAM each) and are weighted toward tsys1/tsys9 to keep the heavy RAM/CPU hosts
(tsys6 with 128 GB, tsys7 with 192 GB) free for large worker nodes. wnodes
run one per hypervisor host across the fleet to maximize total cluster capacity.
pfv-k8s runs all R&D and RackRental (containerlab) workloads via Kubernetes.
Production (Gitea, RustFS, Redmine, websites) lives on a VPS in Reston, VA
running Cloudron -- not in this cluster.
---
## 3. Storage Architecture
### 3.1 NFS exports from tsys4 (primary storage server)
| Export | Disk model | Type | Bus | Total | Used | Free | Use% |
|--------|-----------|------|-----|-------|------|------|------|
| D2 | WDC WD30EFRX Red | HDD (7200rpm) | SATA | 2.7 TB | 187 GB | **2.4 TB** | 8% |
| ~~D3~~ | ~~SK hynix SC300~~ | ~~SSD~~ | ~~USB~~ | — | — | — | **moving to tsys5 Friday** |
| D5 | Hitachi HDS72302 | HDD (7200rpm) | SATA | 1.8 TB | 236 GB | **1.5 TB** | 14% |
Non-exported disks on tsys4:
- sda (Hitachi 1.8T) at /mnt/albert -- not NFS shared, 1.7 TB free
- sdd (WDC 1T) -- **idle, unmounted, removable** (free up for other use)
- sdf (WDC 4.5T SMR) at /mnt/backup -- **PBS backup target**, 4.3 TB free
**D3 migration (Friday):** The SK hynix SC300 SSD is currently USB-attached on
tsys4 (via a "ThinkPad SSD" USB adapter). It is moving to a tsys5 SAS port,
eliminating the USB bottleneck. tsys4's 4 SATA ports are all occupied (sda/sdb
/sdc/sdd), so tsys5 is the better target. See section 3.2.
### 3.2 NFS exports from tsys5 (secondary storage -- becoming the fast-tier hub)
| Export | Disk model | Type | Bus | Total | Used | Free | Use% |
|--------|-----------|------|-----|-------|------|------|------|
| S1 | Seagate ST1000VN | HDD | SAS | 916 GB | 60 GB | 810 GB | 7% |
| S2 | Seagate ST1000VN | HDD | SAS | 916 GB | **6.9 GB** | **863 GB** | **1%** |
| S3 | Seagate ST1000VN | HDD | SAS | 916 GB | 7.0 GB | **863 GB** | **1%** |
| S4 | Toshiba DT01ACA050 | HDD | SAS | 458 GB | 2 MB | **435 GB** | **0%** |
| T5-SSD | Samsung 860 PRO | **SSD** | SAS | 234 GB | **122 GB** | **101 GB** | **55%** |
| **D3** (Friday) | SK hynix SC300 | **SSD** | **SAS** | **469 GB** | **2 MB** | **445 GB** | **0%** |
**tsys5 storage controllers (plenty of free ports):**
- LSI SAS1068E (SAS 6/iR): 8 ports, 3 used (Samsung SSD, Hitachi, Seagate),
**5 free**
- Intel ICH10 SATA #1 (4-port): 2 used (Seagate S3, Toshiba S4), **2 free**
- Intel ICH10 SATA #2 (2-port): **status unknown, likely free**
- 2x Renesas USB 3.0 xHCI controllers (real USB 3.0, unlike tsys6/7)
**Key finding: S2 and S3 now have k8s node disks.** S2 holds cnode5 +
wnode-tsys1 + wnode-tsys9 (6.9 GB used). S3 has wnode-tsys9's old disk
(unused, 7 GB). S4 still 99% empty (435 GB free).
T5-SSD grew to 55% used (122 GB) -- ultix-streaming is the primary consumer.
**Friday additions:**
1. **D3 (SK hynix SSD)** moves from tsys4 USB to tsys5 SAS port. Eliminates
USB 2.0 bottleneck. Becomes the second SSD-tier NFS export.
2. **PCI NVMe drive** (local-only, not NFS-exported). Used for wnode-tsys5
boot disk and HPC scratch. The fastest tier in the fleet.
After Friday, **tsys5 consolidates all fast storage**: NVMe (local) + 2 SSD
NFS exports (D3 + T5-SSD) + 4 HDD NFS exports (S1-S4). This makes tsys5 the
natural home for latency-sensitive workloads and the k8s StorageClass design
center.
### 3.3 Local storage tiers (per host, with utilization)
| Host | Storage ID | Disk type | Bus | Total | Used | Free | Used by VMs? |
|------|-----------|-----------|-----|-------|------|------|-------------|
| tsys1 | local-lvm | HDD | SATA | ~90 GB | low | ~90 GB | No (all VMs on NFS) |
| **tsys3** | **local-lvm** | **NVMe (Samsung PM961)** | **NVMe** | **349 GB** | **0 GB** | **349 GB** | **No (all VMs on NFS)** |
| tsys4 | local-lvm | HDD | SATA | ~94 GB | PBS VM | ~62 GB | Yes (PBS VM 400) |
| tsys5 | local-lvm | HDD (Hitachi 1.8T) | SATA | 1.7 TB | 40 MB | **1.7 TB** | No |
| tsys5 | local-nonprod | HDD (Seagate 1T, =S1) | SATA | 916 GB | 53 GB | **856 GB** | Yes (sectestbed suite) |
| **tsys6** | **local-lvm** | **HDD (WD My Passport)** | **USB 2.0** | **1.7 TB** | **0 GB** | **1.7 TB** | **No -- DO NOT USE for VM storage** |
| **tsys7** | **local-lvm** | **HDD (WD portable)** | **USB 2.0** | **1.7 TB** | **0 GB** | **1.7 TB** | **No -- DO NOT USE for VM storage** |
| **tsys9** | **local-lvm** | **SSD (PNY CS900)** | **SATA** | **136 GB** | **0 GB** | **136 GB** | **No (all VMs on NFS)** |
| **tsys2** | **NVMe** (Samsung 960 PRO) | **NVMe** | **NVMe** | **512 GB** | (Win10) | **512 GB** | **Incoming -- fastest boot tier after tsys5 NVMe** |
| **tsys2** | **SATA SSD** (Samsung 850 EVO) | **SSD** | **SATA** | **1 TB** | (Win10) | **1 TB** | **Incoming -- bulk data on SSD, not rust** |
**CRITICAL WARNING: tsys6 and tsys7 local-lvm is USB 2.0 portable HDD.**
The entire Proxmox OS, swap, and local-lvm on both R610 and R620 run on a
single **USB 2.0-attached WD My Passport portable HDD** (tsys6: "My Passport
260D"; tsys7: "Drive 2657"). Both servers' only USB controllers are EHCI
(USB 2.0, ~480 Mbps). There is **no USB 3.0/xHCI** on either host.
**USB 2.0 practical throughput is ~30-35 MB/s.** This is 3-4x SLOWER than
NFS-over-HDD (~80-120 MB/s). Moving wnode boot disks to local-lvm on these
hosts would **decrease** performance. local-lvm on tsys6/7 must NOT be used
for VM storage.
Additionally, both servers have completely empty internal drive bays:
- **tsys6**: SAS controller present but **DISABLED** in BIOS. No internal
drives.
- **tsys7**: 6-port SATA AHCI controller present, **5 ports EMPTY** (only
DVD-ROM on port 5). No internal drives.
This is a reliability risk beyond performance: the entire host OS boots
from a consumer-grade portable USB drive not designed for 24/7 server use.
**tsys3 correction:** Previously documented as HDD. Actually boots from a
**Samsung PM961 NVMe 512GB SSD** -- the fastest existing local storage in the
fleet. Its 349 GB of local-lvm is excellent for wnode boot disk use.
**Critical observation: every k8s host has 0% used local-lvm.** tsys3
(Samsung PM961 **NVMe**, 349 GB), tsys6 (WD My Passport **USB 2.0** HDD,
1.7 TB), tsys7 (WD portable **USB 2.0** HDD, 1.7 TB) all have unused local
storage.
**However, only tsys3's local-lvm is suitable for VM storage.** tsys6 and
tsys7 local-lvm is USB 2.0 portable HDD (~30-35 MB/s) -- slower than
NFS-over-HDD and unsuitable for wnode boot disks.
### 3.4 Disk speed tiers summary
| Tier | Where | Speed class | Best for |
|------|-------|------------|----------|
| **NVMe** | tsys3 (Samsung PM961), tsys5 (Friday addition), **tsys2 (Samsung 960 PRO 512GB)** | 2000-3500 MB/s | HPC scratch, ETL staging, container runtime, wnode boot, etcd |
| **Local SSD** | tsys9 (PNY CS900, 136 GB), **tsys2 (Samsung 850 EVO 1TB)** | 500 MB/s | wnode boot disk, etcd |
| **NFS-SSD** | tsys5 D3 (SK hynix, **SAS post-Friday**), tsys5 T5-SSD (Samsung) | 200-400 MB/s over NFS | **k8s worker scratch only** (etcd, container cache, ephemeral volumes) |
| **NFS-HDD** | tsys4 D2/D5, tsys5 S1-S4 | 80-120 MB/s over NFS | Bulk data, large disks, non-critical VMs, **wnode boot on tsys6/7** |
| **Local SATA HDD** | tsys1 local-lvm | 100-150 MB/s | Host OS only |
| **USB 2.0 HDD** | tsys6/7 local-lvm (WD My Passport) | **~30-35 MB/s** | **NOTHING -- slower than NFS, do not use for VMs** |
### 3.5 Storage tier characterization per host
| Host | Storage profile | Detail |
|------|----------------|--------|
| **tsys2** | **SSD/NVMe only** | 960 PRO NVMe 512GB + 850 EVO SATA SSD 1TB. No spinning disk. |
| **tsys3** | **NVMe only** | Samsung PM961 NVMe 512GB. No spinning disk. |
| **tsys5** | **Hybrid** (fast-tier hub) | NVMe (local, Friday) + D3 SSD + T5-SSD + S1-S4 HDD |
| **tsys4** | **Bulk/spinning disk only** | D2 HDD 3TB + D5 HDD 2TB. D3 SSD leaving Friday. PBS target on SMR HDD. |
| **tsys9** | **Local SSD + NFS** | PNY CS900 SSD 136GB local + NFS client |
| **tsys1** | **Local HDD + NFS** | Small local-lvm + NFS client |
| **tsys6/7** | **NFS only** | local-lvm is USB 2.0 HDD (unusable for VMs). All VMs on NFS. |
### 3.6 All exports are single-disk with no redundancy
Every NFS export is a single physical disk formatted ext4. No RAID, no ZFS
mirror, no mdraid. A single disk failure takes down every VM whose disk lives
on that export. This applies to **both storage servers** and to the **PBS
backup target** (a single 4.5T SMR drive).
---
## 4. VM Fleet Inventory
### 4.1 Complete VM roster (running VMs only, 40 VMs across 7 hosts)
#### tsys1 (Infrastructure) -- 11 running VMs
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 100 | pfv-bms (HomeAssistant) | 2 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
| 101 | tsys-ca | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
| 103 | **pfv-netinfra-01** | 2 | 2048 | 32 GB | D5 (tsys4 HDD) | NFS |
| 104 | tsys-librenms | 2 | 2048 | 50 GB | D2 (tsys4 HDD) | NFS |
| 105 | tsys-proxmox-datacenter | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
| 106 | **pfv-k8s-cnode3** | 2 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
| 107 | **pfv-k8s-cnode1** | 2 | 4096 | 32 GB | D5 (tsys4 HDD) | NFS |
| 108 | **tsys-ucs-01** | 2x2 | 8000 | 32 GB | D2 (tsys4 HDD) | NFS |
| 109 | tailscale-router | 2 | 2048 | 25 GB | D2 (tsys4 HDD) | NFS |
| 114 | kali-tsys | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
| 117 | tsys-secure-workbench | 2 | 4000 | 32 GB | D2 (tsys4 HDD) | NFS |
#### tsys3 (Kubernetes) -- 1 running VM
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 313 | **pfv-k8s-wnode-tsys3** | 4x2 | 20000 | 32 GB | D5 (tsys4 HDD) | NFS |
#### tsys4 (Storage) -- 1 running VM
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 400 | pfv-proxmox-backup-server | 2 | 2048 | 32 GB | local-lvm | LOCAL |
#### tsys5 (Storage) -- 15 running VMs
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 509 | **pfv-k8s-wnode-tsys5** | 2x4 | 32000 | 32 GB | D2 (tsys4 HDD) | NFS |
| 5101 | sectestbed-siem | 2x2 | 10000 | 132 GB | local-nonprod | LOCAL |
| 5105 | sectestbed-awx | 2x2 | 4096 | 288 GB | local-nonprod | LOCAL |
| 5106 | sectestbed-k8s-cnode | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5107 | sectestbed-k8s-wnode | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5108 | sectestbed-librenms | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5109 | sectestbed-netinfra | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 5111 | ultix-streaming | 2x2 | 9000 | 288 GB | T5-SSD (tsys5 SSD) | NFS-SSD |
| 5112 | ultix-offstage | 2x2 | 6000 | 288 GB | local-lvm | LOCAL |
| 6000 | sectestbed-sandbox | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51010 | sectestbed-tctc | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51011 | sectestbed-cloudron | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51012 | sectestbed-hfnoc | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 51013 | sectestbed-rancherplatform | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
| 53100 | tsys-preprod-awx | 2x2 | 9000 | 160 GB | local-nonprod | LOCAL |
| 53101 | tsys-preprod-siem | 2x2 | 12000 | 32 GB | local-nonprod | LOCAL |
| 53102 | tsys-preprod-rancherplatform | 2x2 | 8000 | 32 GB | local-nonprod | LOCAL |
#### tsys6 (Kubernetes) -- 3 running VMs (1 wnode stopped)
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 100 | pfv-k8s-wnode-tsys6 | 2x2 | 32000 | 32 GB | D5 (tsys4 HDD) | NFS -- **STOPPED** |
| 600 | tsys-awx | 2x2 | 12000 | 32 GB | D2 (tsys4 HDD) | NFS -- **STOPPED** |
| 601 | **pfv-k8s-cnode4** | 4 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
| 602 | **pfv-k8s-cnode5** | 4 | 4096 | 32 GB | D5 (tsys4 HDD) | NFS |
| 603 | **pfv-k8s-cnode2** | 4 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
#### tsys7 (Kubernetes) -- 4 running VMs
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 701 | **pfv-k8s-wnode-tsys7** | 4 | 32000 | 32 GB | D5 (tsys4 HDD) | NFS |
| 702 | hfnoc-uisp | 2x2 | 8000 | 100 GB | D2 (tsys4 HDD) | NFS |
| 703 | rr-middleware | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
| 704 | TCTC | 4 | 6000 | 32 GB | D2 (tsys4 HDD) | NFS |
#### tsys9 (Infrastructure) -- 5 running VMs
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|------|------|-------|----------|------|---------|------|
| 901 | tsys-siem | 2 | 8000 | 132 GB | D2 (tsys4 HDD) | NFS |
| 902 | **tsys-ucs-02** | 2x2 | 8000 | 50 GB | D5 (tsys4 HDD) | NFS |
| 903 | kali-rd | 2 | 2048 | 32 GB | D5 (tsys4 HDD) | NFS |
| 904 | **pfv-netinfra-02** | 2 | 4000 | 32 GB | D2 (tsys4 HDD) | NFS |
| 905 | **pfv-k8s-wnode-tsys9** | 4 | 4096 | 32 GB | S3 (tsys5 HDD) | NFS |
### 4.2 Storage concentration summary
| Storage target | # of running VMs | % of fleet |
|---------------|-----------------|------------|
| **D2 (tsys4 WDC Red 3TB HDD)** | **18** | **45%** |
| D5 (tsys4 Hitachi 2TB HDD) | 9 | 23% |
| local-nonprod (tsys5 local HDD) | 10 | 25% |
| S3 (tsys5 Seagate 1TB HDD) | 1 | 3% |
| T5-SSD (tsys5 Samsung SSD) | 1 | 3% |
| local-lvm (tsys4 local) | 1 | 3% |
**27 of 40 running VMs (68%) store their disks on tsys4 NFS exports.**
If tsys4 goes offline, two-thirds of the fleet loses its storage.
---
## 5. Kubernetes Node Distribution (re-audited 21:50 CDT)
### 5.1 pfv-k8s cnode (control plane) placement -- CURRENT
| VMID | Name | Hypervisor | Storage | NFS Server | Changed? |
|------|------|------------|---------|-----------|----------|
| 906 | cnode1 | **tsys9** | D5 | tsys4 | **MOVED from tsys1** |
| 705 | cnode2 | **tsys7** | D2 | tsys4 | **MOVED from tsys6** |
| 106 | cnode3 | tsys1 | D2 | tsys4 | no change |
| 601 | cnode4 | tsys6 | D2 | tsys4 | no change |
| 706 | cnode5 | **tsys7** | **S2** | **tsys5** | **MOVED from tsys6, storage moved D5→S2** |
**Storage distribution:**
| Storage server | cnodes | Quorum impact if it fails |
|---------------|--------|--------------------------|
| tsys4 (D2+D5) | **4** (cnode1,2,3,4) | Only cnode5 survives = **QUORUM LOST** |
| tsys5 (S2) | **1** (cnode5) | 4 survive = quorum OK |
**Progress:** cnode5 is now on tsys5 (was all 5 on tsys4). But 4-of-5 on tsys4
still means a tsys4 failure loses quorum. **Need 2 more cnodes on tsys5.**
**Host distribution:** cnodes spread across 4 hosts (tsys1, tsys6, tsys7,
tsys9) -- good host diversity.
### 5.2 pfv-k8s wnode (worker) placement -- CURRENT
| VMID | Name | Hypervisor | Storage | NFS Server | RAM | Status | Changed? |
|------|------|------------|---------|-----------|-----|--------|----------|
| 102 | wnode-tsys1 | tsys1 | S2 | tsys5 | 4 GB | **STOPPED** | **NEW** |
| 313 | wnode-tsys3 | tsys3 | D5 | tsys4 | **28 GB** | running | **RAM bumped 20→28** |
| 509 | wnode-tsys5 | tsys5 | D2 | tsys4 | 32 GB | running | no change |
| 100 | wnode-tsys6 | tsys6 | D5 | tsys4 | 32 GB | running | **NOW RUNNING** |
| 701 | wnode-tsys7 | tsys7 | D5 | tsys4 | 32 GB | running | no change |
| 905 | wnode-tsys9 | tsys9 | **S2** | tsys5 | 4 GB | running | **Storage moved S3→S2** |
**One wnode per host achieved** (tsys1,3,5,6,7,9). wnode-tsys1 is created but
stopped. wnode-tsys6 restarted.
**Storage distribution:**
| Storage server | wnodes | Notes |
|---------------|--------|-------|
| tsys4 (D2+D5) | 4 (tsys3,5,6,7) | Still concentrated |
| tsys5 (S2) | 2 (tsys1,tsys9) | Improving |
### 5.3 Summary: k8s node storage distribution
| Storage server | cnodes | wnodes | Total k8s nodes |
|---------------|--------|--------|-----------------|
| **tsys4 NFS** | **4 (80%)** | **4 (67%)** | **8 (73%)** |
| **tsys5 NFS** | **1 (20%)** | **2 (33%)** | **3 (27%)** |
| Local SSD/NVMe | 0 | 0 | 0 (0%) |
**Was 90%/10%. Now 73%/27%.** Improving but still tsys4-heavy. Target: 3
cnodes on each storage server (60/40 or better) so either server failing
leaves quorum intact.
### 5.4 Remaining cnode migration needed for etcd quorum survival
To survive a tsys4 failure with quorum (3 of 5 alive), at least 3 cnodes must
be on tsys5:
| Action | Effect |
|--------|--------|
| Move cnode3 (D2→S3 on tsys5) | 3 cnodes on tsys5, 2 on tsys4. tsys4 fail = 3 survive |
| Move cnode4 (D2→S2 on tsys5) | Same result, different disk |
| Leave cnode1 and one other on tsys4 | tsys5 fail = 4 survive (OK) |
**Simplest path:** migrate cnode3 and cnode4 storage to tsys5 (S3 and S2) via
PDM. Then tsys4 failure leaves cnode5 + cnode3 + cnode4 = 3 of 5 = quorum OK.
### 5.5 Future k8s architecture (next session -- see [K8S.md](K8S.md))
The k8s layer will be tackled soon. Key requirements from user:
- **Platform:** vcluster + Rancher for multi-tenant management
- **Auth:** OIDC to Keycloak (running on Cloudron in Reston, VA production)
- **Workload isolation (vcluster per tenant):**
- RackRental workloads (containerlab network labs)
- Suborbital ITAR (compliance-restricted)
- Suborbital non-ITAR
- Starting Line Productions customer workloads
- **Solar-aware scale-out:** PowerEdge 19xx and 2950 systems (older hardware)
will be brought online during peak solar production for burst capacity.
These older cores/ram supplement the main fleet when power is abundant.
- **WNode sizing:** every Proxmox node will have a wnode. Some nodes will host
both cnodes + wnodes. Worker sizes will vary from small (4 GB, fitting into
leftover host capacity) to large (28-32 GB, consuming most of a host).
- **Friday final audit:** tsys2 will be loaded with Proxmox on Friday, and a
full final audit will be performed at that time (post-NVMe install on tsys5,
post-D3 SSD relocation, post-tsys4 NIC+RAM).
### 5.4 sectestbed k8s nodes (separate from pfv-k8s)
tsys5 also hosts a separate sectestbed kubernetes stack using local storage:
| VMID | Name | Storage |
|------|------|---------|
| 5106 | sectestbed-k8s-cnode | local-nonprod (local HDD) |
| 5107 | sectestbed-k8s-wnode | local-nonprod (local HDD) |
These are on local storage (good -- no NFS dependency) but on a single host's
single local disk (no redundancy). They are isolated from the pfv-k8s cluster.
---
## 6. Storage Redundancy Analysis
### 6.1 -01/-02 infrastructure pair audit
Two -01/-02 pairs exist in the fleet:
**Pair 1: pfv-netinfra (network infrastructure)**
| Role | VMID | Host | Storage | NFS Server |
|------|------|------|---------|-----------|
| -01 | 103 | tsys1 | D5 | **tsys4** |
| -02 | 904 | tsys9 | D2 | **tsys4** |
**Verdict: HOST redundancy OK (different hosts), STORAGE redundancy FAILED.**
Both halves depend on tsys4. If tsys4 goes down, both netinfra VMs lose their
disks. The -02 half should be on an S2/S3/S4 export from tsys5.
**Pair 2: tsys-ucs (Univention Corporate Server)**
| Role | VMID | Host | Storage | NFS Server |
|------|------|------|---------|-----------|
| -01 | 108 | tsys1 | D2 | **tsys4** |
| -02 | 902 | tsys9 | D5 | **tsys4** |
**Verdict: HOST redundancy OK (different hosts), STORAGE redundancy FAILED.**
Same issue. Both halves on tsys4. The -02 half should be on tsys5 storage.
### 6.2 Redundancy principle for paired VMs
For any -01/-02 pair to survive a single storage server failure:
```
-01 VM disk -> tsys4 NFS export (D2/D3/D5)
-02 VM disk -> tsys5 NFS export (S2/S3/S4/T5-SSD)
```
This ensures that losing either tsys4 or tsys5 takes down only one half of
the pair. Currently, **both pairs fail this test** because both halves are on
tsys4.
### 6.3 NFS server failure blast radius
If **tsys4** goes offline (USB NIC failure, disk failure, reboot):
| Impact | Count |
|--------|-------|
| k8s cnodes that lose storage | 5 of 5 (**etcd quorum lost**) |
| k8s wnodes that lose storage | 4 of 5 |
| Infrastructure VMs that lose storage | 11 of 12 on tsys1 (all on D2/D5) |
| Total VMs that lose storage | **27 of 40 (68%)** |
If **tsys5** goes offline:
| Impact | Count |
|--------|-------|
| k8s cnodes that lose storage | 0 of 5 |
| k8s wnodes that lose storage | 1 of 5 |
| Total VMs that lose storage | 1 of 40 (3%) |
**tsys4 is a massive blast-radius liability. tsys5 is barely used.**
Rebalancing VM storage across both servers dramatically reduces risk.
---
## 7. Local SSD/NVMe Opportunity
### 7.1 Available fast tiers (currently unused by k8s)
| Host | Device | Type | Size | Available for VMs? | Currently used by k8s? |
|------|--------|------|------|--------------------|-----------------------|
| tsys4 | D3 (SK hynix SC300, USB) | SSD | 512 GB | Yes (via NFS) | **No** |
| tsys5 | T5-SSD (Samsung 860 PRO) | SSD | 256 GB | Yes (via NFS) | **No** (used by ultix-streaming) |
| tsys5 | **New NVMe (Friday)** | **NVMe** | TBD | **Yes (local or NFS)** | **No** |
| tsys9 | local-lvm (PNY CS900) | SSD | 137 GB free | Yes (local) | **No** |
### 7.2 Why local storage matters for k8s nodes
Kubernetes nodes are latency-sensitive in two specific areas:
1. **etcd (control plane):** etcd writes are synchronous and latency-critical.
On NFS over HDD, every etcd write traverses: VM -> virtio-scsi -> NFS
client -> TCP -> USB dongle (on tsys4) -> ext4 -> spinning disk. Typical
latency: 2-10 ms per write. On local SSD: 0.1-0.5 ms. On NVMe: 0.02-0.1 ms.
This directly affects k8s API responsiveness and pod scheduling speed.
2. **Container image pulls:** Worker nodes pull container images frequently.
On NFS-over-HDD, image layer extraction is seek-bound and slow. Local SSD
eliminates the network hop and reduces seek time. This matters most for
RackRental/containerlab workloads that spin up containers rapidly.
### 7.3 Current waste: tsys9 local SSD
tsys9 has a 250 GB PNY CS900 SSD with 137 GB of LVM-thin space available.
**Zero VMs use it.** All 5 VMs on tsys9 boot from NFS. The local SSD sits
idle. wnode-tsys9 (VM 905) would benefit significantly from local SSD --
its disk is currently on S3 (tsys5 NFS over a Seagate HDD).
### 7.4 Upcoming opportunity: tsys5 NVMe (Friday)
The PCI NVMe being added to tsys5 will be the fastest storage tier in the
fleet. Two placement options:
**Option A: NFS-export the NVMe (shared).** All hosts can use it. Good for
VMs that might need migration. Adds the NFS/network overhead back.
**Option B: Local-only on tsys5.** VMs on tsys5 get full NVMe speed with no
network overhead. Best for k8s wnode-tsys5 and sectestbed VMs. Cannot be
accessed from other hosts.
**Recommendation:** Option B (local-only). k8s worker nodes do not need
shared storage -- pods are ephemeral and reschedule on failure. The NVMe
should be formatted as a Proxmox directory storage (or LVM-thin) on tsys5
and used for local VM images.
---
## 8. Role Alignment Audit
Per the user's intended role taxonomy: tsys1/9 = infrastructure + k8s control
plane; tsys2/3/6/7 = k8s workers; tsys4/5 = storage. Cnodes on tsys1/9 is
**correct by design** (keeps heavy hosts free for workers).
### 8.1 VMs that need to move
| VMID | Name | Current host | Issue | Target |
|------|------|-------------|-------|--------|
| 509 | pfv-k8s-wnode-tsys5 | tsys5 (storage) | Worker on storage host | tsys7 or tsys2 (when online) |
| 905 | pfv-k8s-wnode-tsys9 | tsys9 (infra) | Can stay if small; user decides | tsys9 OK if small wnode |
### 8.2 Host capacity for k8s nodes
| Host | Role | Current k8s nodes | k8s RAM used | RAM total | Headroom |
|------|------|-------------------|-------------|-----------|----------|
| tsys1 | Infra+k8s ctrl | 2 cnodes | 8 GB | 32 GB | ~12 GB (after 11 infra VMs) |
| tsys3 | K8s worker | 1 wnode | 20 GB | 32 GB | ~12 GB |
| tsys6 | K8s worker | 3 cnodes + 1 wnode (stopped) | 12 GB | 128 GB | **~116 GB** |
| tsys7 | K8s worker | 1 wnode | 32 GB | 192 GB | **~160 GB** |
| tsys9 | Infra+k8s ctrl | 1 wnode | 4 GB | 24 GB | ~12 GB (after 4 infra VMs) |
| tsys2 | K8s worker | 0 (incoming) | 0 | 32 GB | ~32 GB |
**tsys6 and tsys7 are dramatically underutilized** -- 116 GB and 160 GB of
free RAM respectively. They should be the primary targets for heavy worker
nodes and ETL/HPC workloads.
### 8.3 tsys6 wnode-tsys6 is stopped
VM 100 (pfv-k8s-wnode-tsys6) is stopped on tsys6. Its disk is on D5 (tsys4
NFS). This wnode should be restarted (or recreated on local-lvm) to restore
cluster capacity.
---
## 9. Network Findings
### 9.1 tsys9 storage NIC is a USB dongle (new finding)
Validating tsys9 revealed that its storage network interface
(`enx9c69d36a5b6c`) is USB-attached (`parentbus usb`). This is the same
anti-pattern as tsys4. The onboard Intel NIC (`enp0s31f6`) is used for
management; storage uses the USB adapter.
**Impact:** Same as tsys4 -- achieves line rate but is susceptible to cable
wobble, ESD, and USB controller resets. For an infrastructure host with 5
VMs, this is a reliability risk.
**Mitigation:** tsys9 is an OptiPlex 7080 SFF -- it has PCIe slots. A
PCIe NIC would eliminate this risk (same recommendation as tsys4).
### 9.2 tsys4 and tsys5 still blocked (Friday hardware work)
| Host | Blocker | Staged fix |
|------|---------|-----------|
| tsys4 | USB cdc_ncm storage NIC | PCIe NIC install + RAM upgrade (16 to 64 GB) |
| tsys5 | bond0 broken (1 of 2 slaves) | Plug 2nd ethernet cable + apply layer3+4 hash |
### 9.3 LACP resolved on tsys6/tsys7
tsys6 to tsys7 storage path now measures **1.83 Gbps** (was 943 Mbps).
The switch LACP hash change took effect after renegotiation. The 56-106K
retransmits on this path are confirmed to be non-lossy multi-flow TCP-over-
LACP overhead. See `RESULTS.md` (not yet created) for the full analysis.
### 9.4 NFS nconnect=4 + noatime confirmed active
All hosts (including tsys9) show `nconnect=4,noatime` in their NFS mount
options. Each host maintains 4 TCP connections per NFS mount to each storage
server. This was the Tier 0 tuning item from the performance optimization
engagement and is confirmed working cluster-wide.
---
## 10. Recommendations
**No changes have been made. These are assessment-only recommendations.**
See `K8S.md` for the detailed kubernetes architecture deep-dive.
### 10.1 CRITICAL: Critical infrastructure VM placement (netinfra, UCS)
These are the most critical production VMs in the fleet. They must survive
any single-point failure (host, storage server, or disk).
**Design principle for -01/-02 HA pairs:**
- Different hypervisors (already satisfied: tsys1 vs tsys9)
- Different storage servers (currently FAILED: all on tsys4)
- Prefer SSD for latency-sensitive services
**Recommended placement:**
| VM | Host | Storage | Tier | Free space | Rationale |
|----|------|---------|------|-----------|-----------|
| **netinfra-01** (103) | tsys1 | **D5 (tsys4 HDD)** | NFS-HDD | 1.5 TB | DNS/DHCP/NTP = minimal I/O. Stays put. |
| **netinfra-02** (904) | tsys9 | **S3 (tsys5 HDD)** | NFS-HDD | 870 GB | Move from D2. Cross-server redundancy. Minimal I/O. |
| **ucs-01** (108) | tsys1 | **D2 (tsys4 HDD)** | NFS-HDD | 2.4 TB | Stays put. LDAP/AD does not need SSD. |
| **ucs-02** (902) | tsys9 | **S2 (tsys5 HDD)** | NFS-HDD | 870 GB | Move from D5. Cross-server redundancy. No SSD needed. |
**Failure survival matrix (all single-point failures):**
| Failure | netinfra-01 | netinfra-02 | ucs-01 | ucs-02 | Result |
|---------|-------------|-------------|--------|--------|--------|
| tsys4 dies | dies (D5) | **alive** (S3) | dies (D2) | **alive** (S2) | netinfra-02 + ucs-02 alive |
| tsys5 dies | **alive** (D5) | dies (S3) | **alive** (D2) | dies (S2) | netinfra-01 + ucs-01 alive |
| tsys1 dies | dies | **alive** | dies | **alive** | -02 pair survives |
| tsys9 dies | **alive** | dies | **alive** | dies | -01 pair survives |
| Any single disk | **all 4 on different disks/servers** | **all 4 alive** | | | |
**Why this works:** Every row has at least one netinfra and one UCS alive.
The network (DNS/DHCP) and directory (AD/LDAP) services never go fully dark.
**D3 SSD repurposed:** With UCS staying on HDD, the D3 SSD (moving to tsys5
Friday) is freed for latency-sensitive workloads that actually benefit from
SSD -- sectestbed k8s nodes, CI/CD artifact cache, or a container image
registry mirror. Not infrastructure VMs.
### 10.2 CRITICAL: Cnode (control plane) storage split
**Problem:** All 5 cnodes store disks on tsys4. D2 disk failure loses etcd
quorum (3 of 5 cnodes share D2).
**Target: cnodes weighted toward tsys1/tsys9 (lightweight hosts), freeing
tsys6/tsys7 for heavy workers. Storage splits across tsys4 and tsys5.**
| cnode | Current host | **Target host** | Current storage | **Target storage** | Rationale |
|-------|-------------|----------------|----------------|-------------------|-----------|
| cnode1 (107) | tsys1 | **tsys1** (stays) | D5 (tsys4) | **D5 (tsys4)** -- no change | Already correct |
| cnode3 (106) | tsys1 | **tsys1** (stays) | D2 (tsys4) | **S2 (tsys5)** -- **MOVE disk** | Split storage to tsys5 |
| cnode2 (603) | tsys6 | **tsys9** | D2 (tsys4) | **D2 (tsys4)** -- no disk change | Free tsys6 for heavy workers |
| cnode4 (601) | tsys6 | **tsys9** | D2 (tsys4) | **D5 (tsys4)** -- spread disk | Free tsys6; spread off D2 |
| cnode5 (602) | tsys6 | **tsys3** | D5 (tsys4) | **S3 (tsys5)** -- **MOVE disk** | Free tsys6; split storage to tsys5 |
**Result after changes:**
| Host | cnodes | Storage server |
|------|--------|---------------|
| tsys1 | cnode1 (D5), cnode3 (S2) | tsys4 + tsys5 |
| tsys9 | cnode2 (D2), cnode4 (D5) | tsys4 |
| tsys3 | cnode5 (S3) | tsys5 |
- 3 cnodes on tsys4 storage, 2 on tsys5. Either storage server can fail and
etcd keeps quorum (3 of 5 survive).
- D2 has 1 cnode (was 3). D5 has 2. S2 and S3 have 1 each. No single disk
holds more than 2 cnodes.
- Cnodes now on 3 hosts (tsys1, tsys9, tsys3). Any single host failure leaves
at least 3 cnodes alive.
- **tsys6 and tsys7 are fully freed** for heavy worker nodes.
Note: tsys1 RAM is tight (32 GB, 11 infra VMs). Adding 0 new cnodes (keeping
the 2 already there) is feasible with KSM. tsys9 (24 GB) has room for 2
cnodes (8 GB). tsys3 (32 GB) has room for 1 cnode (4 GB) alongside its wnode.
### 10.3 HIGH: Wnode distribution -- one per host, tiered storage
**Target: one wnode per hypervisor host, using local storage where possible
and NFS-HDD for bulk data.**
| wnode | Host | Boot disk (OS+containers) | Data disk (bulk/ETL) | Rationale |
|-------|------|--------------------------|---------------------|-----------|
| wnode-tsys1 | tsys1 | D5 (tsys4 NFS) | -- | Small wnode on infra host. Minimal capacity. |
| wnode-tsys3 | tsys3 | **local-lvm (349 GB NVMe)** | S3 (NFS) | Move from NFS to **NVMe** (Samsung PM961). Fastest boot disk after tsys5/2. |
| wnode-tsys6 | tsys6 | D2 (tsys4 NFS) | -- | **Stays on NFS.** local-lvm is USB 2.0 HDD (~30 MB/s) -- slower than NFS. |
| wnode-tsys7 | tsys7 | D5 (tsys4 NFS) | -- | **Stays on NFS.** local-lvm is USB 2.0 HDD (~30 MB/s) -- slower than NFS. |
| wnode-tsys9 | tsys9 | **local-lvm (136 GB SSD)** | S2 (NFS) | Move from NFS to local SSD. Fast boot, NFS for bulk. |
| wnode-tsys2 | tsys2 | **NVMe (Samsung 960 PRO 512GB)** | **SATA SSD (Samsung 850 EVO 1TB)** | **Best storage of any wnode.** No NFS needed -- 1.5 TB local SSD. |
| wnode-tsys5 | tsys5 | **new NVMe (local)** | local-nonprod (HDD) | **Fastest wnode in fleet.** HPC/ETL workloads land here. |
**Storage tiering strategy per wnode:**
Each wnode gets two disk tiers mapped to k8s StorageClasses:
1. **Boot + container runtime** (local-lvm or NVMe): OS, kubelet, container
images, ephemeral storage. This is where local SSD/NVMe shines -- container
image pulls and layer extraction are seek-bound and benefit enormously from
low-latency storage.
2. **Bulk data** (NFS-HDD via D2/D5/S2/S3): weather/GIS datasets, ETL staging
areas, large files that do not fit on local storage. Mounted as a second
disk in the VM and exposed to k8s as a StorageClass.
This maps to two k8s StorageClasses:
- `local-storage`: bound to the wnode's boot/local disk (fast, ephemeral)
- `nfs-bulk`: bound to NFS exports (slow, persistent, large capacity)
### 10.4 HIGH: Dedicate D3 SSD exclusively to k8s scratch
**Storage philosophy (user directive): NVMe/SSD is for k8s worker scratch
space and ultix-streaming (developer workstation running "cluster of 1"
pre-production jobs). Spinning rust hosts all other infrastructure VMs**
(UCS, netinfra, LibreNMS, SIEM, etc.).
SSD allocation after Friday:
- **D3 (tsys5 SAS, 445 GB free, 0% used)** -- dedicated to k8s scratch via
the `nfs-ssd` StorageClass (etcd, container cache, ephemeral volumes).
- **T5-SSD (tsys5 SAS, 140 GB free)** -- ultix-streaming (VM 5111) stays here
(developer workstation, runs single-node test jobs before k8s). Remaining
140 GB available for k8s.
**Deep-dive on exact k8s scratch allocation is deferred to the next session**
(K8S.md) once we know the k8s distribution, job scheduler, and workload mix.
### 10.5 MEDIUM: Restart wnode-tsys6
VM 100 (pfv-k8s-wnode-tsys6) is stopped on tsys6. Recreate on local-lvm
(1.7 TB free) instead of D5 NFS. This restores cluster capacity and moves
the boot disk to local storage simultaneously.
### 10.6 MEDIUM: tsys5 NVMe placement (Friday)
**Recommendation: local-only on tsys5, formatted as Proxmox LVM-thin or
directory storage.**
Use for:
- wnode-tsys5 boot disk (primary beneficiary -- HPC/ETL workloads)
- sectestbed VMs that need fast scratch space
- Not NFS-exported (avoid adding network overhead to the fastest tier)
### 10.7 LOW: Add PCIe NIC to tsys9
tsys9's storage NIC is a USB dongle. tsys9 is an OptiPlex 7080 with PCIe
slots. A $150 PCIe NIC eliminates the USB reliability risk.
### 10.8 LOW: Standardize PVE/kernel versions
tsys3 is on PVE kernel 7.0.14; others on 6.17.x. PVE-manager versions vary
(9.1.1 / 9.1.5 / 9.2.5). Standardize in a maintenance window.
---
## 11. Hardware End-of-Support Exposure
| Host | EOS date | Years past | Form factor |
|------|----------|-----------|-------------|
| pfv-tsys6 (R610) | 2013-05 | 13.2 | 1U server |
| pfv-tsys5 (T7500) | 2014-12 | 11.7 | Workstation |
| pfv-tsys4 (T1700) | 2018-03 | 8.4 | Workstation |
| pfv-tsys7 (R620) | 2019-03 | 7.4 | 1U server |
| pfv-tsys1 (9020) | 2019-07 | 7.0 | SFF desktop |
| pfv-tsys3 (7510) | 2020-07 | 6.0 | Laptop |
| pfv-tsys2 (5520) | TBD | -- | Laptop |
| **pfv-tsys9 (7080)** | **2024-02** | **2.4** | **SFF desktop (only supported)** |
**6 of 8 hosts are past vendor end-of-support.** Only tsys9 is still covered.
The two actual rack servers (R610, R620) are the most overdue for replacement.
This belongs in the next budget cycle.
---
## 12. Open Items
### 12.1 Immediate (do today via PDM)
1. **Migrate ucs-02** (VM 902) from D5 (tsys4) to S2 (tsys5) for cross-server
redundancy. UCS stays on HDD.
2. **Migrate netinfra-02** (VM 904) from D2 (tsys4) to S3 (tsys5).
3. **Migrate cnode3** (VM 106) from D2 (tsys4) to S3 (tsys5) -- etcd quorum.
4. **Migrate cnode4** (VM 601) from D2 (tsys4) to S2 (tsys5) -- etcd quorum.
5. **Start wnode-tsys1** (VM 102) if the cluster needs the capacity.
### 12.2 Friday maintenance window (user action)
1. **tsys4:** Install PCIe NIC (replace USB dongle), add RAM (16 to 64 GB),
reconfigure `/etc/network/interfaces`, reboot.
2. **tsys5:** Plug 2nd ethernet cable, verify bond0, apply layer3+4 hash,
install PCI NVMe, relocate D3 SSD from tsys4 USB to tsys5 SAS port,
format NVMe as local storage, reboot.
3. **tsys2:** Load Proxmox (replacing Windows 10).
4. **Final audit:** Re-run `deploy-check.sh` across all hosts including tsys2.
### 12.3 Post-Friday validation
1. Re-run iperf matrix: `./iperf-full-matrix.sh`
2. Validate tsys4 and tsys5: `./validate-fixes.sh pfv-tsys4 && ./validate-fixes.sh pfv-tsys5`
3. Run `scripts/check.sh` on tsys2 once Proxmox is loaded.
4. Update PROJECT.md with post-hardware numbers.
### 12.4 Future: Kubernetes deep-dive (see [K8S.md](K8S.md))
Next major workstream. Requirements captured:
- **vcluster + Rancher** for multi-tenant k8s management
- **OIDC auth** to Keycloak (on Cloudron, Reston VA production)
- **Workload isolation** via separate vclusters:
- RackRental (containerlab)
- Suborbital ITAR
- Suborbital non-ITAR
- Starting Line Productions customer workloads
- **Solar-aware scale-out:** PowerEdge 19xx + 2950 systems brought online
during peak solar production for burst capacity
- **Every host gets a wnode** (variable sizing: small 4 GB to large 32 GB)
- **SSD/NVMe reserved for k8s scratch** (plus ultix-streaming exception)
- **Spinning rust for all infrastructure VMs**
### 12.5 Data gaps
| Gap | How to close |
|-----|--------------|
| tsys5 SDR/parallel-port workload dependency | Confirm what uses the SDR |
| tsys2 post-Proxmox baseline | Run `check.sh` after Friday install |
| tsys5 NVMe size and model | Confirm after Friday installation |
| tsys3 thermal state (laptop in rack) | Check `sensors` on next maintenance |
| PowerEdge 19xx/2950 inventory | When solar scale-out is planned |
### 12.6 Scripts and tools available
| Script | Purpose |
|--------|---------|
| `scripts/check.sh` | Read-only data collector (run on hosts) |
| `scripts/apply-tunings.sh` | Apply all Tier 0 tunings (dry-run/apply/rollback) |
| `scripts/fix-bond-nfs.sh` | Fix NFS options + bond hash |
| `validate-fixes.sh` | Read-only validation of all applied changes |
| `iperf-full-matrix.sh` | Full iperf test suite (mgmt + storage) |
| `deploy-check.sh` | Deploy check.sh to all hosts via SSH (now includes tsys9) |
+242
View File
@@ -0,0 +1,242 @@
# TODO.md — Pending User Actions
**Date:** 2026-07-27
**Items needing user input or physical action.**
---
## 1. tsys2 Windows hardware inventory (run on the Windows host)
pfv-tsys2 is currently Windows 10. Before rebuilding it as Proxmox, gather
hardware data so the architecture plan can account for it.
### Option A: PowerShell (recommended — single command, copy-paste output)
Open **PowerShell as Administrator** and run:
```powershell
# Full hardware inventory in one shot
Write-Output "=== COMPUTER ==="
Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer, Model, SystemType, TotalPhysicalMemory | Format-List
Write-Output "`n=== CPU ==="
Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed | Format-List
Write-Output "`n=== MEMORY STICKS ==="
Get-CimInstance Win32_PhysicalMemory | Select-Object Manufacturer, PartNumber, Capacity, Speed, ConfiguredClockSpeed, DeviceLocator, FormFactor | Format-Table -AutoSize
Write-Output "`n=== DISKS ==="
Get-PhysicalDisk | Select-Object FriendlyName, MediaType, BusType, Size, SpindleSpeed | Format-Table -AutoSize
Write-Output "`n=== DISK PARTITIONS ==="
Get-Disk | Select-Object Number, FriendlyName, Size, PartitionStyle, OperationalStatus | Format-Table -AutoSize
Write-Output "`n=== NETWORK ADAPTERS ==="
Get-NetAdapter | Select-Object Name, InterfaceDescription, Status, LinkSpeed, MacAddress | Format-Table -AutoSize
Write-Output "`n=== GPU(s) ==="
Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor | Format-List
Write-Output "`n=== PCIe SLOTS ==="
Get-CimInstance Win32_SystemSlot | Select-Object SlotDesignation, CurrentUsage, Status | Format-Table -AutoSize
Write-Output "`n=== USB DEVICES (storage + network only) ==="
Get-PnpDevice -PresentOnly | Where-Object { $_.Class -in @('DiskDrive','Net','USB') } | Select-Object Class, FriendlyName, Status | Format-Table -AutoSize
```
Copy the full output into a file (e.g., `tsys2-hardware.txt`) or paste it
directly into the chat.
### Option B: Command Prompt (cmd.exe) fallbacks
If PowerShell is unavailable for some reason, these cmd commands give a
subset:
```cmd
:: Computer model and serial
wmic computersystem get manufacturer,model
wmic bios get serialnumber
:: CPU
wmic cpu get name,numberofcores,numberoflogicalprocessors,maxclockspeed
:: RAM (total)
wmic computersystem get totalphysicalmemory
:: RAM sticks (per-slot detail)
wmic memorychip get manufacturer,capacity,speed,partnumber,devicelocator
:: Disks
wmic diskdrive get model,size,interfacetype,mediatype
:: Network adapters
wmic nic where netenabled=true get name,speed,macaddress
:: GPU
wmic path win32_videocontroller get name,adapterram,driverversion
```
### What I'm looking for
- **Disk inventory**: Are there any SSDs/NVMe available locally? (Determines
whether wnode-tsys2 can use local storage like the other wnodes.)
- **Network adapters**: How many onboard NICs? Model? (Determines whether
tsys2 needs a USB dongle for storage network like tsys4/9, or has a real
onboard NIC available.)
- **RAM layout**: Is all 32 GB in 1 stick, 2 sticks, or 4 sticks? (Affects
memory bandwidth for HPC workloads — dual-channel matters.)
- **GPU detail**: Confirm the Quadro M1200 model and VRAM for passthrough
planning.
- **PCIe slots**: Is there a free PCIe slot for adding a NIC or HBA?
- **Service tag confirmation**: `GH1XZG2` (already on file from spreadsheet).
### STATUS: Collected 2026-07-27
**Disk inventory — RESOLVED:**
- Disk 0: Samsung SSD 960 PRO **512 GB NVMe** (best local storage in fleet)
- Disk 1: Samsung SSD 850 EVO **1 TB SATA SSD**
- Both SSDs, no spinning rust. 1.5 TB total local SSD.
**Network adapters — RESOLVED (concerning):**
- StorageNetwork: **ASIX USB to Gigabit Ethernet** (dongle, D4-81-D7-3E-0D-5E)
- Ethernet: **Realtek USB GbE Family Controller** (also USB, 18-FD-CB-00-D2-CA)
- Wi-Fi: Intel 8265 (disconnected)
- **Both wired NICs are USB-attached.** Same anti-pattern as tsys4/9.
Unavoidable on this laptop form factor — no onboard PCIe NIC available.
**GPU — RESOLVED:**
- Intel HD Graphics 630 (integrated, 1 GB)
- NVIDIA Quadro M1200 (4 GB, confirmed for passthrough)
**PCIe slots — RESOLVED:**
- Slots 3/6/7/8 report "Available" but these are laptop M.2/WWAN slots, not
user-accessible full PCIe. **Cannot add a PCIe NIC.** NVMe slot occupied
by 960 PRO.
**RAM — PARTIAL:**
- Total 32 GB confirmed (34,097,573,888 bytes).
- Per-stick detail failed to run (PowerShell line-break split
`Format-T` + `able`). Re-run the command below if bandwidth planning
needs stick-level detail:
```powershell
Get-CimInstance Win32_PhysicalMemory | Select-Object Manufacturer, PartNumber, Capacity, Speed, ConfiguredClockSpeed, DeviceLocator, FormFactor | Format-Table -AutoSize
```
**Service tag — CONFIRMED:** `GH1XZG2` (Precision 5520).
---
## 2. Friday maintenance window (physical hardware)
### tsys4 — install PCIe NIC + add RAM
1. Power down tsys4 (graceful shutdown via Proxmox UI or `shutdown -h now`).
2. Install the **PCIe NIC** (Intel i350-T2 or similar 1 GbE dual-port).
3. Add **RAM**: 16 GB → 64 GB DDR3 ECC.
4. Power on, then update `/etc/network/interfaces` to replace
`enx8cae4ccda926` (USB dongle) with the new PCIe NIC device name.
5. Reboot to activate new NIC and NFS nconnect.
6. Run `validate-fixes.sh pfv-tsys4` to confirm.
### tsys5 — plug storage cable + install NVMe + relocate D3 SSD
1. Plug the **second ethernet cable** into tsys5's dedicated storage NIC.
2. Verify bond0 recovery: `cat /proc/net/bonding/bond0` — look for
"Number of ports: 2" and a real partner MAC (not all zeros).
3. Apply bond hash fix (same as tsys6/7):
```bash
echo "layer3+4" > /sys/class/net/bond0/bonding/xmit_hash_policy
```
4. **Relocate D3 SSD** from tsys4 USB to tsys5 SAS port:
- Power down tsys4
- Remove the SK hynix SC300 SSD from its USB enclosure on tsys4
- Install it on a free SAS port on tsys5 (5 ports free on LSI SAS1068E)
- On tsys5: mount as `/mnt/pfv-tsys5/D3`, add to `/etc/exports`
- Update `/etc/pve/storage.cfg` cluster-wide: repoint D3 `server` from
`pfv-tsys4-nfs-stor` to `pfv-tsys5-nfs-stor`, update `export` path
- Copy any existing D3 data from tsys4 first (currently ~2 MB, essentially
empty, so minimal migration)
5. Install the **PCI NVMe drive** (uses a PCI slot, not a SATA/SAS port).
6. Format NVMe as local directory storage (see TODO section 3 below).
7. Reboot tsys5 to activate NFS nconnect.
8. Run `validate-fixes.sh pfv-tsys5` to confirm.
---
## 3. tsys5 NVMe format/mount decision (after Friday install)
**Recommendation: local-only, not NFS-exported.** Format as Proxmox directory
storage so it shows up as a VM image target in the Proxmox UI.
After the NVMe is physically installed and visible in Proxmox:
1. Identify the device: `lsblk` or `ls /dev/nvme*`
2. Format and add to Proxmox:
```bash
# Option A: LVM-thin (thin provisioning, snapshots)
pvcreate /dev/nvme0n1
vgcreate nvme-pool /dev/nvme0n1
lvcreate -l 100%FREE -T nvme-pool/data
# Then in Proxmox UI: Datacenter > Storage > Add > LVM-Thin
# ID: nvme-local
# Volume Group: nvme-pool
# Thin Pool: data
# Content: Disk image, Container template
# Option B: Directory (simpler, no thin provisioning)
mkfs.ext4 /dev/nvme0n1
mkdir -p /mnt/nvme
mount /dev/nvme0n1 /mnt/nvme
# Add to /etc/fstab for persistence
# Then in Proxmox UI: Datacenter > Storage > Add > Directory
# ID: nvme-local
# Directory: /mnt/nvme
# Content: Disk image, Container template
```
3. Use for wnode-tsys5 boot disk (highest impact) and sectestbed VM scratch.
---
## 4. Post-hardware validation (run after Friday work)
1. Re-run iperf matrix: `./iperf-full-matrix.sh`
2. Validate tsys4 and tsys5: `./validate-fixes.sh pfv-tsys4 && ./validate-fixes.sh pfv-tsys5`
3. Update PROJECT.md with post-hardware iperf numbers.
---
## 5. UCS storage migration to spinning disk (do today)
UCS (Univention Corporate Server / open-source AD) does not need SSD. Both
UCS VMs should stay on spinning disk (HDD) and be split across storage
servers for redundancy.
| VM | Current | Target | Action |
|----|---------|--------|--------|
| ucs-01 (108) | D2 (tsys4 HDD) | **D2 (tsys4 HDD) -- no change** | Already correct |
| ucs-02 (902) | D5 (tsys4 HDD) | **S2 (tsys5 HDD)** | Move for cross-server redundancy |
**To migrate ucs-02 to S2 (use PDM/Proxmox UI):**
1. In Proxmox Datacenter or the node UI, select VM 902 on tsys9
2. Use "Migrate" or "Storage Migrate" to move the disk from D5 to S2
(both are NFS exports visible to tsys9, so this is a storage-only migration)
3. Verify VM 902 boots and LDAP/AD services are healthy after migration
Note: both VMs are currently on tsys4 HDD, which is fine for UCS. Only
ucs-02 needs to move -- it should be on a different storage server than
ucs-01 so a tsys4 failure doesn't take down both halves of the AD pair.
---
## 6. Open questions for next session
- Are the hosts a Proxmox cluster (`pvecm status`) or standalone installs?
Determines whether live migration is available.
- What k8s distribution is in use? (k3s, kubeadm, RKE2?)
- Container runtime? (containerd, cri-o?)
- Is there a local container image registry mirror?
- What specific ETL tools? (GDAL, PostGIS, xarray, Dask?)
- HPC job scheduler? (plain k8s Jobs, Argo Workflows, Volcano?)
- What uses tsys5's SDR + parallel port before planning tsys5 role changes?
- tsys3 thermal state (laptop in rack for years) — check `sensors`.
+335 -9
View File
@@ -1,10 +1,336 @@
# docs/server-build/DEPLOYMENT.md
# TSYS FetchApply Deployment Guide
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Server deployment procedures**
>
> **Read it here:** https://community.turnsys.com/t/302
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
## Overview
This guide provides comprehensive instructions for deploying the TSYS FetchApply infrastructure provisioning system on Linux servers.
## Prerequisites
### System Requirements
- **Operating System:** Ubuntu 18.04+ or Debian 10+ (recommended)
- **RAM:** Minimum 2GB, recommended 4GB
- **Disk Space:** Minimum 10GB free space
- **Network:** Internet connectivity for package downloads
- **Privileges:** Root or sudo access required
### Required Tools
- `git` - Version control system
- `curl` - HTTP client for downloads
- `wget` - Alternative download tool
- `systemctl` - System service management
- `apt-get` - Package management (Debian/Ubuntu)
### Network Requirements
- **HTTPS access** to:
- `https://archive.ubuntu.com` (Ubuntu packages)
- `https://linux.dell.com` (Dell hardware support)
- `https://download.proxmox.com` (Proxmox packages)
- `https://github.com` (Git repositories)
## Pre-Deployment Validation
### 1. System Compatibility Check
```bash
# Clone repository
git clone [repository-url]
cd FetchApply
# Run system validation
./Project-Tests/validation/system-requirements.sh
```
### 2. Network Connectivity Test
```bash
# Test network connectivity
curl -I https://archive.ubuntu.com
curl -I https://linux.dell.com
curl -I https://download.proxmox.com
```
### 3. Permission Verification
```bash
# Verify write permissions
test -w /etc && echo "✅ /etc writable" || echo "❌ /etc not writable"
test -w /usr/local/bin && echo "✅ /usr/local/bin writable" || echo "❌ /usr/local/bin not writable"
```
## Deployment Methods
### Method 1: Standard Deployment (Recommended)
```bash
# 1. Clone repository
git clone [repository-url]
cd FetchApply
# 2. Run pre-deployment tests
./Project-Tests/run-tests.sh validation
# 3. Execute deployment
cd ProjectCode
sudo bash SetupNewSystem.sh
```
### Method 2: Dry Run Mode
```bash
# 1. Clone repository
git clone [repository-url]
cd FetchApply
# 2. Review configuration
cat provisioning/SetupNewSystem.sh
# 3. Execute with manual review
cd ProjectCode
sudo bash -x SetupNewSystem.sh # Debug mode
```
## Deployment Process
### Phase 1: Framework Initialization
1. **Environment Setup**
- Load framework variables
- Source framework includes
- Initialize logging system
2. **System Detection**
- Detect physical vs virtual hardware
- Identify operating system
- Check for existing users
### Phase 2: Base System Configuration
1. **Package Installation**
- Update package repositories
- Install essential packages
- Configure package sources
2. **User Management**
- Create required user accounts
- Configure SSH access
- Set up sudo permissions
### Phase 3: Security Hardening
1. **SSH Configuration**
- Deploy hardened SSH configuration
- Install SSH keys
- Disable password authentication
2. **System Hardening**
- Configure firewall rules
- Enable audit logging
- Install security tools
### Phase 4: Monitoring and Management
1. **Monitoring Agents**
- Deploy LibreNMS agents
- Configure SNMP
- Set up system monitoring
2. **Management Tools**
- Install Cockpit dashboard
- Configure remote access
- Set up maintenance scripts
## Post-Deployment Verification
### 1. Security Validation
```bash
# Run security tests
./Project-Tests/run-tests.sh security
# Verify SSH configuration
ssh -T [server-ip] # Should work with key authentication
```
### 2. Service Status Check
```bash
# Check critical services
sudo systemctl status ssh
sudo systemctl status auditd
sudo systemctl status snmpd
```
### 3. Network Connectivity
```bash
# Test internal services
curl -k https://localhost:9090 # Cockpit
snmpwalk -v2c -c public localhost system
```
## Troubleshooting
### Common Issues
#### 1. Permission Denied Errors
```bash
# Solution: Run with sudo
sudo bash SetupNewSystem.sh
```
#### 2. Network Connectivity Issues
```bash
# Check DNS resolution
nslookup archive.ubuntu.com
# Test direct IP access
curl -I 91.189.91.26 # Ubuntu archive IP
```
#### 3. Package Installation Failures
```bash
# Update package cache
sudo apt-get update
# Fix broken packages
sudo apt-get -f install
```
#### 4. SSH Key Issues
```bash
# Verify key permissions
ls -la ~/.ssh/
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub
```
### Debug Mode
```bash
# Enable debug logging
export DEBUG=1
bash -x SetupNewSystem.sh
```
### Log Analysis
```bash
# Check deployment logs
tail -f /var/log/fetchapply/deployment.log
# Review system logs
journalctl -u ssh
journalctl -u auditd
```
## Environment-Specific Configurations
### Physical Dell Servers
- **OMSA Installation:** Dell OpenManage Server Administrator
- **Hardware Monitoring:** iDRAC configuration
- **Performance Tuning:** CPU and memory optimizations
### Virtual Machines
- **Guest Additions:** VMware tools or VirtualBox additions
- **Resource Limits:** Memory and CPU constraints
- **Network Configuration:** Bridge vs NAT settings
### Development Environments
- **SSH Configuration:** Less restrictive settings
- **Development Tools:** Additional packages for development
- **Testing Access:** Enhanced logging and debugging
## Maintenance and Updates
### Regular Maintenance
```bash
# Update system packages
sudo apt-get update && sudo apt-get upgrade
# Update monitoring scripts
cd /usr/local/bin
sudo wget https://[repository]/scripts/up2date.sh
sudo chmod +x up2date.sh
```
### Security Updates
```bash
# Check for security updates
sudo apt-get update
sudo apt list --upgradable | grep -i security
# Apply security patches
sudo apt-get upgrade
```
### Configuration Updates
```bash
# Update FetchApply
cd FetchApply
git pull origin main
# Re-run specific modules
cd provisioning/Modules/Security
sudo bash secharden-ssh.sh
```
## Best Practices
### 1. Pre-Deployment
- Always test in non-production environment first
- Review all scripts before execution
- Validate network connectivity
- Ensure proper backup procedures
### 2. During Deployment
- Monitor deployment progress
- Check for errors and warnings
- Document any customizations
- Validate each phase completion
### 3. Post-Deployment
- Run full security test suite
- Verify all services are running
- Test remote access
- Document deployment specifics
### 4. Ongoing Operations
- Regular security updates
- Monitor system performance
- Review audit logs
- Maintain deployment documentation
## Support and Resources
### Documentation
- **README.md:** Basic usage instructions
- **SECURITY.md:** Security architecture and guidelines
- **tests/README.md:** Testing framework documentation
### Community Support
- **Issues:** https://projects.knownelement.com/project/reachableceo-vptechnicaloperations/timeline
- **Discussion:** https://community.turnsys.com/c/chieftechnologyandproductofficer/26
### Professional Support
- **Technical Support:** [Contact information to be added]
- **Consulting Services:** [Contact information to be added]
## Deployment Checklist
### Pre-Deployment
- [ ] System requirements validated
- [ ] Network connectivity tested
- [ ] Backup procedures in place
- [ ] Security review completed
### Deployment
- [ ] Repository cloned successfully
- [ ] Pre-deployment tests passed
- [ ] Deployment executed without errors
- [ ] Post-deployment verification completed
### Post-Deployment
- [ ] Security tests passed
- [ ] All services running
- [ ] Remote access verified
- [ ] Documentation updated
### Maintenance
- [ ] Update schedule established
- [ ] Monitoring configured
- [ ] Backup procedures tested
- [ ] Incident response plan activated
## Version History
- **v1.0:** Initial deployment framework
- **v1.1:** Added security hardening and secrets management
- **v1.2:** Enhanced testing framework and documentation
Last updated: July 14, 2025
+406 -9
View File
@@ -1,10 +1,407 @@
# docs/server-build/DEVELOPMENT-GUIDELINES.md
<!-- Historical AI-generated review. Paths may reference pre-merge structure. -->
# TSYS PFVCluster Development Guidelines
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Coding standards, commit conventions, script patterns**
>
> **Read it here:** https://community.turnsys.com/t/302
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
## Overview
This document contains development standards and best practices for the TSYS PFVCluster infrastructure provisioning system.
## Package Management Best Practices
### Combine apt-get Install Commands
**Rule:** Always combine multiple package installations into a single `apt-get install` command for performance.
**Rationale:** Single command execution is significantly faster than multiple separate commands due to:
- Reduced package cache processing
- Single dependency resolution
- Fewer network connections
- Optimized package download ordering
#### ✅ Correct Implementation
```bash
# Install all packages in one command
apt-get install -y package1 package2 package3 package4
# Real example from 2FA script
apt-get install -y libpam-google-authenticator qrencode
```
#### ❌ Incorrect Implementation
```bash
# Don't use separate commands for each package
apt-get install -y package1
apt-get install -y package2
apt-get install -y package3
```
#### Complex Package Installation Pattern
```bash
function install_security_packages() {
print_info "Installing security packages..."
# Update package cache once
apt-get update
# Install all packages in single command
apt-get install -y \
auditd \
fail2ban \
libpam-google-authenticator \
lynis \
rkhunter \
aide \
chkrootkit \
clamav \
clamav-daemon
print_success "Security packages installed successfully"
}
```
## Script Development Standards
### Error Handling
- Always use `set -euo pipefail` at script start
- Implement proper error trapping
- Use framework error handling functions
- Return appropriate exit codes
### Function Structure
```bash
function function_name() {
print_info "Description of what function does..."
# Local variables
local var1="value"
local var2="value"
# Function logic
if [[ condition ]]; then
print_success "Success message"
return 0
else
print_error "Error message"
return 1
fi
}
```
### Framework Integration
- Source framework includes at script start
- Use framework logging and pretty print functions
- Follow existing patterns for consistency
- Include proper PROJECT_ROOT path resolution
```bash
# Standard framework sourcing pattern
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
source "$PROJECT_ROOT/Framework-Includes/PrettyPrint.sh"
source "$PROJECT_ROOT/Framework-Includes/Logging.sh"
source "$PROJECT_ROOT/Framework-Includes/ErrorHandling.sh"
```
## Code Quality Standards
### ShellCheck Compliance
- All scripts must pass shellcheck validation
- Address shellcheck warnings appropriately
- Use proper quoting for variables
- Handle edge cases and error conditions
### Variable Naming
- Use UPPERCASE for global constants
- Use lowercase for local variables
- Use descriptive names
- Quote all variable expansions
```bash
# Global constants
declare -g BACKUP_DIR="/root/backup"
declare -g CONFIG_FILE="/etc/ssh/sshd_config"
# Local variables
local user_name="localuser"
local temp_file="/tmp/config.tmp"
# Proper quoting
if [[ -f "$CONFIG_FILE" ]]; then
cp "$CONFIG_FILE" "$BACKUP_DIR/"
fi
```
### Function Documentation
- Include purpose description
- Document parameters if any
- Document return values
- Include usage examples for complex functions
```bash
# Configure SSH hardening settings
# Parameters: none
# Returns: 0 on success, 1 on failure
# Usage: configure_ssh_hardening
function configure_ssh_hardening() {
print_info "Configuring SSH hardening..."
# Implementation
}
```
## Testing Requirements
### Test Coverage
- Every new module must include corresponding tests
- Test both success and failure scenarios
- Validate configurations after changes
- Include integration tests for complex workflows
### Test Categories
1. **Unit Tests:** Individual function validation
2. **Integration Tests:** Module interaction testing
3. **Security Tests:** Security configuration validation
4. **Validation Tests:** System requirement checking
### Test Implementation Pattern
```bash
function test_function_name() {
echo "🔍 Testing specific functionality..."
local failed=0
# Test implementation
if [[ condition ]]; then
echo "✅ Test passed"
else
echo "❌ Test failed"
((failed++))
fi
return $failed
}
```
## Security Standards
### Configuration Backup
- Always backup configurations before modification
- Use timestamped backup directories
- Provide restore instructions
- Test backup/restore procedures
### Service Management
- Test configurations before restarting services
- Provide rollback procedures
- Validate service status after changes
- Include service dependency handling
### User Safety
- Use `nullok` for gradual 2FA rollout
- Provide clear setup instructions
- Include emergency access procedures
- Test all access methods before enforcement
## Documentation Standards
### Script Headers
```bash
#!/bin/bash
# TSYS Module Name - Brief Description
# Longer description of what this script does
# Author: TSYS Development Team
# Version: 1.0
# Last Updated: YYYY-MM-DD
set -euo pipefail
```
### Inline Documentation
- Comment complex logic
- Explain non-obvious decisions
- Document external dependencies
- Include troubleshooting notes
### User Documentation
- Create comprehensive guides for complex features
- Include step-by-step procedures
- Provide troubleshooting sections
- Include examples and use cases
## Performance Optimization
### Package Management
- Single apt-get commands (as noted above)
- Cache package lists appropriately
- Use specific package versions when stability required
- Clean up package cache when appropriate
### Network Operations
- Use connection timeouts for external requests
- Implement retry logic with backoff
- Cache downloaded resources when possible
- Validate download integrity
### File Operations
- Use efficient file processing tools
- Minimize file system operations
- Use appropriate file permissions
- Clean up temporary files
## Version Control Practices
### Commit Messages
- Use descriptive commit messages
- Include scope of changes
- Reference related issues/requirements
- Follow established commit message format
### Branch Management
- Test changes in feature branches
- Use pull requests for review
- Maintain clean commit history
- Tag releases appropriately
### Code Review Requirements
- All changes require review
- Security changes require security team review
- Test coverage must be maintained
- Documentation must be updated
## Deployment Practices
### Pre-Deployment
- Run full test suite
- Validate in test environment
- Review security implications
- Update documentation
### Deployment Process
- Use configuration validation
- Implement gradual rollout when possible
- Monitor for issues during deployment
- Have rollback procedures ready
### Post-Deployment
- Validate deployment success
- Monitor system performance
- Update operational documentation
- Gather feedback for improvements
## Example Implementation
### Complete Module Template
```bash
#!/bin/bash
# TSYS Security Module - Template
# Template for creating new security modules
# Author: TSYS Development Team
set -euo pipefail
# Source framework functions
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
source "$PROJECT_ROOT/Framework-Includes/PrettyPrint.sh"
source "$PROJECT_ROOT/Framework-Includes/Logging.sh"
source "$PROJECT_ROOT/Framework-Includes/ErrorHandling.sh"
# Module configuration
BACKUP_DIR="/root/backup/module-$(date +%Y%m%d-%H%M%S)"
CONFIG_FILE="/etc/example.conf"
# Create backup directory
mkdir -p "$BACKUP_DIR"
print_header "TSYS Module Template"
function backup_configs() {
print_info "Creating configuration backup..."
if [[ -f "$CONFIG_FILE" ]]; then
cp "$CONFIG_FILE" "$BACKUP_DIR/"
print_success "Configuration backed up"
fi
}
function install_packages() {
print_info "Installing required packages..."
# Update package cache
apt-get update
# Install all packages in single command
apt-get install -y package1 package2 package3
print_success "Packages installed successfully"
}
function configure_module() {
print_info "Configuring module..."
# Configuration logic here
print_success "Module configured successfully"
}
function validate_configuration() {
print_info "Validating configuration..."
local failed=0
# Validation logic here
if [[ $failed -eq 0 ]]; then
print_success "Configuration validation passed"
return 0
else
print_error "Configuration validation failed"
return 1
fi
}
function main() {
# Check if running as root
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
exit 1
fi
# Execute module steps
backup_configs
install_packages
configure_module
validate_configuration
print_success "Module setup completed successfully!"
}
# Run main function
main "$@"
```
## Continuous Improvement
### Regular Reviews
- Review guidelines quarterly
- Update based on lessons learned
- Incorporate new best practices
- Gather team feedback
### Tool Updates
- Keep development tools current
- Adopt new security practices
- Update testing frameworks
- Improve automation
### Knowledge Sharing
- Document lessons learned
- Share best practices
- Provide training materials
- Maintain knowledge base
---
**Last Updated:** July 14, 2025
**Version:** 1.0
**Author:** TSYS Development Team
**Note:** These guidelines are living documents and should be updated as the project evolves and new best practices are identified.
+189 -9
View File
@@ -1,10 +1,190 @@
# docs/server-build/SECURITY.md
# PFVCluster Security Documentation
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Security architecture: SSH hardening, 2FA, SCAP-STIG, Wazuh, auditd**
>
> **Read it here:** https://community.turnsys.com/t/303
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
## Security Architecture
The PFVCluster infrastructure provisioning system is designed with security-first principles, implementing multiple layers of protection for server deployment and management.
## Current Security Features
### 1. Secure Deployment Method ✅
- **Git-based deployment:** Uses `git clone` instead of `curl | bash`
- **Local execution:** Scripts run locally after inspection
- **Version control:** Full audit trail of changes
- **Code review:** Changes require explicit approval
### 2. HTTPS Enforcement ✅
- **All downloads use HTTPS:** Eliminates man-in-the-middle attacks
- **SSL certificate validation:** Automatic certificate checking
- **Secure repositories:** Ubuntu archive, Dell, Proxmox all use HTTPS
- **No HTTP fallbacks:** No insecure download methods
### 3. SSH Hardening
- **Key-only authentication:** Password login disabled
- **Secure ciphers:** Modern encryption algorithms only
- **Fail2ban protection:** Automated intrusion prevention
- **Custom SSH configuration:** Hardened sshd_config
### 4. System Security
- **Firewall configuration:** Automated iptables rules
- **Audit logging:** auditd with custom rules
- **SIEM integration:** Wazuh agent deployment
- **Compliance scanning:** SCAP-STIG automated checks
### 5. Error Handling
- **Bash strict mode:** `set -euo pipefail` prevents errors
- **Centralized logging:** All operations logged with timestamps
- **Graceful failures:** Proper cleanup on errors
- **Line-level debugging:** Error reporting with line numbers
## Security Testing
### Automated Security Validation
```bash
# Run security test suite
./tests/run-tests.sh security
# Specific security tests
./tests/security/https-enforcement.sh
```
### Security Test Categories
1. **HTTPS Enforcement:** Validates all URLs use HTTPS
2. **Deployment Security:** Checks for secure deployment methods
3. **SSL Certificate Validation:** Tests certificate authenticity
4. **Permission Validation:** Verifies proper file permissions
## Threat Model
### Mitigated Threats
- **Supply Chain Attacks:** Git-based deployment with review
- **Man-in-the-Middle:** HTTPS-only downloads
- **Privilege Escalation:** Proper permission models
- **Unauthorized Access:** SSH hardening and key management
### Remaining Risks
- **Secrets in Repository:** SSH keys stored in git (planned for removal)
- **No Integrity Verification:** Downloads lack checksum validation
- **No Backup/Recovery:** No rollback capability implemented
## Security Recommendations
### High Priority
1. **Implement Secrets Management**
- Remove SSH keys from repository
- Use Bitwarden/Vault for secret storage
- Implement key rotation procedures
2. **Add Download Integrity Verification**
- SHA256 checksum validation for all downloads
- GPG signature verification where available
- Fail-safe on integrity check failures
3. **Enhance Audit Logging**
- Centralized log collection
- Real-time security monitoring
- Automated threat detection
### Medium Priority
1. **Configuration Backup**
- System state snapshots before changes
- Rollback capability for failed deployments
- Configuration drift detection
2. **Network Security**
- VPN-based deployment (where applicable)
- Network segmentation for management
- Encrypted communication channels
## Compliance
### Security Standards
- **CIS Benchmarks:** Automated compliance checking
- **STIG Guidelines:** SCAP-based validation
- **Industry Best Practices:** Following NIST cybersecurity framework
### Audit Requirements
- **Change Tracking:** All modifications logged
- **Access Control:** Permission-based system access
- **Vulnerability Management:** Regular security assessments
## Incident Response
### Security Event Handling
1. **Detection:** Automated monitoring and alerting
2. **Containment:** Immediate isolation procedures
3. **Investigation:** Log analysis and forensics
4. **Recovery:** System restoration procedures
5. **Lessons Learned:** Process improvement
### Contact Information
- **Security Team:** [To be defined]
- **Incident Response:** [To be defined]
- **Escalation Path:** [To be defined]
## Security Development Lifecycle
### Code Review Process
1. **Static Analysis:** Automated security scanning
2. **Peer Review:** Manual code inspection
3. **Security Testing:** Automated security test suite
4. **Approval:** Security team sign-off
### Deployment Security
1. **Pre-deployment Validation:** Security test execution
2. **Secure Deployment:** Authorized personnel only
3. **Post-deployment Verification:** Security configuration validation
4. **Monitoring:** Continuous security monitoring
## Security Tools and Integrations
### Current Tools
- **Wazuh:** SIEM and security monitoring
- **Lynis:** Security auditing
- **auditd:** System call auditing
- **Fail2ban:** Intrusion prevention
### Planned Integrations
- **Vault/Bitwarden:** Secrets management
- **OSSEC:** Host-based intrusion detection
- **Nessus/OpenVAS:** Vulnerability scanning
- **ELK Stack:** Log aggregation and analysis
## Vulnerability Management
### Vulnerability Scanning
- **Regular scans:** Monthly vulnerability assessments
- **Automated patching:** Security update automation
- **Exception handling:** Risk-based patch management
- **Reporting:** Executive security dashboards
### Disclosure Process
1. **Internal Discovery:** Report to security team
2. **Assessment:** Risk and impact evaluation
3. **Remediation:** Patch development and testing
4. **Deployment:** Coordinated security updates
5. **Verification:** Post-patch validation
## Security Metrics
### Key Performance Indicators
- **Deployment Success Rate:** Percentage of successful secure deployments
- **Vulnerability Response Time:** Time to patch critical vulnerabilities
- **Security Test Coverage:** Percentage of code covered by security tests
- **Incident Response Time:** Time to detect and respond to security events
### Monitoring and Reporting
- **Real-time Dashboards:** Security status monitoring
- **Executive Reports:** Monthly security summaries
- **Compliance Reports:** Quarterly compliance assessments
- **Trend Analysis:** Security posture improvement tracking
## Contact and Support
For security-related questions or incidents:
- **Repository Issues:** https://projects.knownelement.com/project/reachableceo-vptechnicaloperations/timeline
- **Community Discussion:** https://community.turnsys.com/c/chieftechnologyandproductofficer/26
- **Security Team:** [Contact information to be added]
## Security Updates
This document is updated as security features are implemented and threats evolve. Last updated: July 14, 2025.
+328 -9
View File
@@ -1,10 +1,329 @@
# docs/server-build/TSYS-2FA-GUIDE.md
# TSYS Two-Factor Authentication Implementation Guide
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **End-user guide for 2FA setup**
>
> **Read it here:** https://community.turnsys.com/t/303
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
## Overview
This guide provides complete instructions for implementing and managing two-factor authentication (2FA) on TSYS servers using Google Authenticator (TOTP).
## What This Implementation Provides
### Services Protected by 2FA
- **SSH Access:** Requires SSH key + 2FA token
- **Cockpit Web Interface:** Requires password + 2FA token
- **Webmin Administration:** Requires password + 2FA token (if installed)
### Security Features
- **Time-based One-Time Passwords (TOTP):** Standard 6-digit codes
- **Backup Codes:** Emergency access codes
- **Gradual Rollout:** Optional nullok mode for phased deployment
- **Configuration Backup:** Automatic backup of all configs
## Implementation Steps
### Step 1: Run the 2FA Setup Script
```bash
# Navigate to the security modules directory
cd provisioning/Modules/Security
# Run the 2FA setup script as root
sudo bash secharden-2fa.sh
```
### Step 2: Validate Installation
```bash
# Run 2FA validation tests
./Project-Tests/security/2fa-validation.sh
# Run specific 2FA security test
./Project-Tests/run-tests.sh security
```
### Step 3: Setup Individual Users
For each user that needs 2FA access:
```bash
# Check setup instructions
cat /home/username/2fa-setup-instructions.txt
# Run user setup script
sudo /tmp/setup-2fa-username.sh
```
### Step 4: Test 2FA Access
1. **Test SSH access** from another terminal
2. **Test Cockpit access** via web browser
3. **Test Webmin access** if installed
## User Setup Process
### Installing Authenticator Apps
Users need one of these apps on their phone:
- **Google Authenticator** (Android/iOS)
- **Authy** (Android/iOS)
- **Microsoft Authenticator** (Android/iOS)
- **1Password** (with TOTP support)
### Setting Up 2FA for a User
1. **Run setup script:**
```bash
sudo /tmp/setup-2fa-username.sh
```
2. **Follow prompts:**
- Answer "y" to update time-based token
- Scan QR code with authenticator app
- Save emergency backup codes securely
- Answer "y" to remaining security questions
3. **Test immediately:**
```bash
# Test SSH from another terminal
ssh username@server-ip
# You'll be prompted for 6-digit code
```
## Configuration Details
### SSH Configuration Changes
File: `/etc/ssh/sshd_config`
```
ChallengeResponseAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive
```
### PAM Configuration
File: `/etc/pam.d/sshd`
```
auth required pam_google_authenticator.so nullok
```
### Cockpit Configuration
File: `/etc/cockpit/cockpit.conf`
```
[WebService]
LoginTitle = TSYS Server Management
LoginTo = 300
RequireHost = true
[Session]
Banner = /etc/cockpit/issue.cockpit
IdleTimeout = 15
```
### Webmin Configuration
File: `/etc/webmin/miniserv.conf`
```
twofactor_provider=totp
twofactor=1
```
## Security Considerations
### Gradual vs Strict Enforcement
#### Gradual Enforcement (Default)
- Uses `nullok` option in PAM
- Users without 2FA can still log in
- Allows phased rollout
- Good for initial deployment
#### Strict Enforcement
- Remove `nullok` from PAM configuration
- All users must have 2FA configured
- Immediate security enforcement
- Risk of lockout if misconfigured
### Backup and Recovery
#### Emergency Access
- **Backup codes:** Generated during setup
- **Root access:** Can disable 2FA if needed
- **Console access:** Physical/virtual console bypasses SSH
#### Configuration Backup
- Automatic backup to `/root/backup/2fa-TIMESTAMP/`
- Includes all modified configuration files
- Can be restored if needed
## Troubleshooting
### Common Issues
#### 1. User Cannot Generate QR Code
```bash
# Ensure qrencode is installed
sudo apt-get install qrencode
# Re-run user setup
sudo /tmp/setup-2fa-username.sh
```
#### 2. SSH Connection Fails
```bash
# Check SSH service status
sudo systemctl status sshd
# Test SSH configuration
sudo sshd -t
# Check logs
sudo journalctl -u sshd -f
```
#### 3. 2FA Code Not Accepted
- **Check time synchronization** on server and phone
- **Verify app setup** - rescan QR code if needed
- **Try backup codes** if available
#### 4. Locked Out of Server
```bash
# Access via console (physical/virtual)
# Disable 2FA temporarily
sudo cp /root/backup/2fa-*/pam.d.bak/sshd /etc/pam.d/sshd
sudo systemctl restart sshd
```
### Debug Commands
```bash
# Check 2FA status
./Project-Tests/security/2fa-validation.sh
# Check SSH configuration
sudo sshd -T | grep -E "(Challenge|PAM|Authentication)"
# Check PAM configuration
cat /etc/pam.d/sshd | grep google-authenticator
# Check user 2FA status
ls -la ~/.google_authenticator
```
## Management and Maintenance
### Adding New Users
1. Ensure user account exists
2. Run setup script for new user
3. Provide setup instructions
4. Test access
### Removing User 2FA
```bash
# Remove user's 2FA configuration
sudo rm /home/username/.google_authenticator
# User will need to re-setup 2FA
```
### Disabling 2FA System-Wide
```bash
# Restore original configurations
sudo cp /root/backup/2fa-*/sshd_config.bak /etc/ssh/sshd_config
sudo cp /root/backup/2fa-*/pam.d.bak/sshd /etc/pam.d/sshd
sudo systemctl restart sshd
```
### Updating 2FA Configuration
```bash
# Re-run setup script
sudo bash secharden-2fa.sh
# Validate changes
./Project-Tests/security/2fa-validation.sh
```
## Best Practices
### Deployment Strategy
1. **Test in non-production** environment first
2. **Enable gradual rollout** (nullok) initially
3. **Train users** on 2FA setup process
4. **Test emergency procedures** before strict enforcement
5. **Monitor logs** for authentication issues
### Security Recommendations
- **Enforce strict mode** after successful rollout
- **Regular backup code rotation**
- **Monitor failed authentication attempts**
- **Document emergency procedures**
- **Regular security audits**
### User Training
- **Provide clear instructions**
- **Demonstrate setup process**
- **Explain backup code importance**
- **Test login process with users**
- **Establish support procedures**
## Monitoring and Logging
### Authentication Logs
```bash
# SSH authentication logs
sudo journalctl -u sshd | grep -i "authentication"
# PAM authentication logs
sudo journalctl | grep -i "pam_google_authenticator"
# Failed login attempts
sudo journalctl | grep -i "failed"
```
### Security Monitoring
- Monitor for repeated failed 2FA attempts
- Alert on successful logins without 2FA (during gradual rollout)
- Track user 2FA setup completion
- Monitor for emergency access usage
## Integration with Existing Systems
### LDAP/Active Directory
- 2FA works with existing authentication systems
- Users still need local 2FA setup
- Consider centralized 2FA solutions for large deployments
### Monitoring Systems
- LibreNMS: Will continue to work with SNMP
- Wazuh: Will log 2FA authentication events
- Cockpit: Enhanced with 2FA protection
### Backup Systems
- Ensure backup procedures account for 2FA
- Test restore procedures with 2FA enabled
- Document emergency access procedures
## Support and Resources
### Files Created by Setup
- `/tmp/setup-2fa-*.sh` - User setup scripts
- `/home/*/2fa-setup-instructions.txt` - User instructions
- `/root/backup/2fa-*/` - Configuration backups
### Validation Tools
- `./Project-Tests/security/2fa-validation.sh` - Complete 2FA validation
- `./Project-Tests/run-tests.sh security` - Security test suite
### Emergency Contacts
- System Administrator: [Contact Info]
- Security Team: [Contact Info]
- 24/7 Support: [Contact Info]
## Compliance and Audit
### Security Benefits
- Significantly reduces risk of unauthorized access
- Meets multi-factor authentication requirements
- Provides audit trail of authentication events
- Complies with security frameworks (NIST, ISO 27001)
### Audit Trail
- All authentication attempts logged
- 2FA setup events recorded
- Configuration changes tracked
- Emergency access documented
---
**Last Updated:** July 14, 2025
**Version:** 1.0
**Author:** TSYS Security Team
+111 -9
View File
@@ -1,10 +1,112 @@
# docs/server-build/tailscale.md
# Tailscale vs. Managed DNS — Architecture Analysis
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Tailscale vs managed DNS analysis (resolved)**
>
> **Read it here:** https://community.turnsys.com/t/306
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
> **Status:** **RESOLVED.** The pfv-netinfra-01/02 pair now runs production
> Technitium DNS with all `knel.net` records replicated from tailscale-router
> via the DNS cluster setup. Both LAN IPs serve authoritative records for
> `knel.net` and recurse externally. This document records the original
> conflict, how it was resolved, and the recommended client configuration.
## 1. Executive summary
Every host in this build runs the Tailscale client, and Tailscale's MagicDNS
manages `/etc/resolv.conf` by default (pointing at `100.100.100.100`). This
previously conflicted with a managed `resolv.conf` pointing at the LAN
resolvers. The root cause was that the LAN Technitium instances did not have
the `knel.net` zone populated — **that is now fixed.**
The pfv-netinfra-01/02 pair now serves identical, authoritative `knel.net`
records (replicated from production via [`dns-cluster-setup/`](../dns-cluster-setup/README.md)).
Both LAN IPs resolve `knel.net` device names and recurse externally. The
managed `resolv.conf` is now safe to deploy.
**Recommendation:** Deploy the managed `resolv.conf` (`.252`/`.253`) on hosts
where you want tunnel-independent DNS. Leave Tailscale managing DNS on hosts
where MagicDNS device names must resolve without a LAN path (e.g. laptops off
-network). See [§5](#5-recommendation) for details.
## 2. How name resolution works today (post-cluster-setup)
Probed from `sectestbed-sandbox` (192.168.3.50) after the DNS cluster was
deployed:
| Query path | External name (`github.com`) | `knel.net` device name (`pfv-netinfra-01.knel.net`) |
|---|---|---|
| Via Tailscale resolver (`100.100.100.100`) | resolves | resolves → `100.70.181.72` (Tailscale CGNAT) |
| Direct `dig @192.168.3.252` (Technitium primary, LAN) | resolves (recurses) | **resolves**`100.70.181.72` |
| Direct `dig @192.168.3.253` (Technitium secondary, LAN) | resolves (recurses) | **resolves**`100.70.181.72` |
**Both LAN resolvers now serve `knel.net` records identically.** The
Technitium zone is no longer stale — it was replicated from production
(tailscale-router) as part of the DNS cluster setup.
### What changed
Previously (before the DNS cluster setup), querying the LAN IPs returned
NXDOMAIN for `knel.net` device names because the Technitium `knel.net` zone
was empty (SOA serial `2025062313`, dated 2025-06-23). After replicating
production config to both netinfra hosts, all 124 zones — including
`knel.net` with all current device records — are served authoritatively on
both `.252` and `.253`.
## 3. The DNS server pair
| Host | IP | Role | Services |
|------|----|------|----------|
| pfv-netinfra-01 | 192.168.3.252 | **Primary** | Technitium (authoritative, port 5300) + Pi-hole (recursive, port 53) |
| pfv-netinfra-02 | 192.168.3.253 | **Secondary** | Technitium (replicated via rsync, port 5300) + Pi-hole (recursive, port 53) |
Zone replication is rsync-based (every 60s via systemd timer) because
Technitium's AXFR uses port 53, which is occupied by Pi-hole on these hosts.
See [`dns-cluster-setup/README.md`](../dns-cluster-setup/README.md) for
full details.
## 4. NTP (fully resolved)
NTP is independent of DNS: `provisioning/ConfigFiles/NTP/ntp.conf` points
directly at the LAN IPs with no DNS dependency:
```
server 192.168.3.252 iburst
server 192.168.3.253 iburst
```
Both servers respond with stratum 2/3. The client config uses `restrict`
rules (not `interface listen`) to avoid the loopback-binding bug that
prevented sync. This is safe under both Tailscale-managed and LAN-pinned
resolver configurations.
## 5. Recommendation
### On fixed servers (always on-LAN)
**Deploy the managed `resolv.conf`** (`provisioning/ConfigFiles/Resolv/`):
- Points at `.252`/`.253` with failover
- `knel.net` records resolve on both servers
- External names recurse on both servers
- DNS survives `tailscaled` outages (unlike Tailscale-managed DNS)
To prevent Tailscale from overwriting the managed file:
```bash
tailscale up --accept-dns=false
```
### On laptops / roaming hosts
**Let Tailscale manage DNS** (default `accept-dns=true`):
- MagicDNS resolves `knel.net` device names via the tunnel
- No dependency on LAN reachability
- Accept the `tailscaled` dependency (if the tunnel is down, you're off-network anyway)
## 6. Known items / future work
1. **Pi-hole upstream configuration.** Pi-hole on both hosts should forward
to the local Technitium instance (port 5300) for `knel.net` and to an
external resolver for everything else. Verify this is configured on both
nodes.
2. **Zone transfer via AXFR.** Currently using rsync because Technitium's
AXFR expects port 53. If Technitium's listen port can be changed, or
Pi-hole can be configured to proxy AXFR, the rsync timer could be
replaced with native DNS zone transfer.
3. **`accept-dns=false` automation.** The provisioning code should set
`--accept-dns=false` on Tailscale during setup (after deploying the
managed `resolv.conf`) so Tailscale doesn't overwrite it on reboot.
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env bash
# hooks/ticket-gate.sh — enforce ticket-first work policy
#
# Blocks modifying operations until an active ticket is established.
# The agent sets the active ticket via: echo '#NNN' > .crush/active-ticket
# And clears it when done: > .crush/active-ticket
#
# Exempts read-only and management commands (so you can create tickets,
# run audits, check status, etc.).
set -euo pipefail
TICKET_FILE="${CRUSH_PROJECT_DIR}/.crush/active-ticket"
TOOL="${CRUSH_TOOL_NAME:-}"
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
# Read-only tools — always allowed
case "$TOOL" in
view|ls|grep|glob|agent|sourcegraph|fetch|agentic_fetch|download|lsp_diagnostics|lsp_symbols|lsp_definition|lsp_references|lsp_call_hierarchy|crush_info|crush_logs|question|todos)
exit 0
;;
esac
# For bash tool: exempt read-only and management commands
if [ "$TOOL" = "bash" ]; then
# Ticket/doc/dns management — always allowed
case "$CMD" in
*"redmine-cli"*|*"discourse-cli"*|*"dns-cli"*|*"technitium"*) exit 0 ;;
esac
# Read-only git
case "$CMD" in
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
esac
# Repo hygiene scripts
case "$CMD" in
*"check-rules"*|*"setup-hooks"*|*"shellcheck"*|*"run-tests"*) exit 0 ;;
esac
# Monitoring/probe commands
case "$CMD" in
*"tailscale status"*|*"access-matrix"*) exit 0 ;;
esac
# Setting/clearing the active ticket
case "$CMD" in
*active-ticket*) exit 0 ;;
esac
fi
# For edit/write: exempt policy/hook files (these ARE the policy)
FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
case "$FILE_PATH" in
*/AGENTS.md|*/check-rules.sh|*/crush.json|*/hooks/*)
if [ "$TOOL" = "write" ] || [ "$TOOL" = "edit" ] || [ "$TOOL" = "multiedit" ]; then
exit 0
fi
;;
esac
# Check for active ticket
if [ -f "$TICKET_FILE" ] && [ -s "$TICKET_FILE" ]; then
TICKET=$(cat "$TICKET_FILE")
printf '{"context":"Active ticket: %s"}\n' "$TICKET"
exit 0
fi
# No active ticket — block
cat >&2 <<'MSG'
TICKET GATE: No active ticket set.
This project requires ticket-governed work (AGENTS.md Agent Authority).
Before modifying systems or code, set the active ticket:
echo '#NNN' > .crush/active-ticket
If no ticket exists yet, create one first (redmine-cli create), then set it.
Clear the ticket when work is complete:
> .crush/active-ticket
MSG
exit 2
+54 -9
View File
@@ -1,10 +1,55 @@
# k8s/README.md
# k8s/ — pfv-k8s Cluster Setup Scripts
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **k3s cluster setup scripts: wipe, bootstrap, taint, verify**
>
> **Read it here:** https://community.turnsys.com/t/305
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
Scripts to bootstrap and manage the k3s control plane on cnode1/2/3.
All cluster communication goes over Tailscale IPs — no LAN traffic.
## Current State
3-node HA control plane (k3s v1.36.2+k3s1, embedded etcd):
| Node | Tailscale IP | Role | Tainted |
|------|-------------|------|---------|
| pfv-k8s-cnode1 | 100.97.178.106 | control-plane, etcd | NoSchedule |
| pfv-k8s-cnode2 | 100.109.34.72 | control-plane, etcd | NoSchedule |
| pfv-k8s-cnode3 | 100.106.222.18 | control-plane, etcd | NoSchedule |
## Scripts
| Script | Purpose |
|--------|---------|
| [`env.sh`](env.sh) | Shared config: node IPs, SSH opts, k3s version. Sourced by all scripts. |
| [`wipe.sh`](wipe.sh) | Remove existing k3s from all cnodes (clean slate). |
| [`install-cp.sh`](install-cp.sh) | Full bootstrap: cnode1 (--cluster-init) then cnode2/3 join. |
| [`join-servers.sh`](join-servers.sh) | Re-join cnode2/3 only (if cnode1 is already up). |
| [`post-setup.sh`](post-setup.sh) | Apply NoSchedule taints, fetch kubeconfig, verify. |
| [`verify.sh`](verify.sh) | Health check: nodes Ready, Tailscale IPs, taints, etcd, CoreDNS. |
| [`probe-nodes.sh`](probe-nodes.sh) | Verify SSH + Tailscale reachability. |
## Usage
```bash
# Full bootstrap from scratch:
bash k8s/wipe.sh
bash k8s/install-cp.sh
bash k8s/post-setup.sh
bash k8s/verify.sh
# Access the cluster:
export KUBECONFIG=~/.kube/config.pfv-k8s
kubectl get nodes
```
## Design Decisions
- **k3s (not Talos):** This is a regular R&D cluster, not ITAR/classified.
Talos architecture is documented in [`docs/k8s/`](../docs/k8s/) for when
that requirement comes online. k3s on stock Debian is simpler to operate.
- **Tailscale-only transport:** `--node-ip`, `--advertise-address`, and
`--tls-san` are all set to Tailscale IPs. No LAN IP appears in any node
status or certificate.
- **VXLAN flannel:** Pods communicate via flannel VXLAN overlay on top of
Tailscale's WireGuard. Double-encrypted, but functional and reliable.
- **NoSchedule taint:** All 3 cnodes are tainted so no user workloads
schedule on the control plane. Only system components (CoreDNS,
metrics-server, flannel, kube-proxy) with built-in tolerations run here.
- **Embedded etcd:** 3-node HA etcd quorum. Tolerates 1 node failure.
-10
View File
@@ -1,10 +0,0 @@
# k8s/docs/ARCHITECTURE.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **k8s target architecture: control plane, network, identity, storage, DR**
>
> **Read it here:** https://community.turnsys.com/t/305
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-10
View File
@@ -1,10 +0,0 @@
# k8s/docs/DISTRO-DECISION.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Talos vs k3s distro analysis and decision**
>
> **Read it here:** https://community.turnsys.com/t/305
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-10
View File
@@ -1,10 +0,0 @@
# k8s/docs/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **k8s docs index + TL;DR**
>
> **Read it here:** https://community.turnsys.com/t/305
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
+3 -30
View File
@@ -1,12 +1,11 @@
#!/usr/bin/bash
# shellcheck disable=SC2034 # sourced config file; variables are consumed by scripts that source this
# k8s/env.sh — shared config for all k8s scripts. Source this.
#
# All cluster communication goes over Tailscale IPs. No LAN IPs, ever.
# --- Control plane nodes (Tailscale 100.x addresses) ---
# --- Nodes (Tailscale 100.x addresses) ---
CNODE1_NAME="pfv-k8s-cnode1"
CNODE1_IP="100.125.134.53"
CNODE1_IP="100.97.178.106"
CNODE2_NAME="pfv-k8s-cnode2"
CNODE2_IP="100.109.34.72"
@@ -21,32 +20,6 @@ ALL_CNODE_NAMES=("$CNODE1_NAME" "$CNODE2_NAME" "$CNODE3_NAME")
BOOTSTRAP_IP="$CNODE1_IP"
BOOTSTRAP_NAME="$CNODE1_NAME"
# --- Worker nodes (Tailscale 100.x addresses) ---
WNODE1_NAME="pfv-k8s-wnode-tsys3"
WNODE1_IP="100.126.9.112"
WNODE2_NAME="pfv-k8s-wnode-tsys5"
WNODE2_IP="100.122.252.116"
WNODE3_NAME="pfv-k8s-wnode-tsys6"
WNODE3_IP="100.83.49.75"
WNODE4_NAME="pfv-k8s-wnode-tsys7"
WNODE4_IP="100.119.240.11"
WNODE5_NAME="pfv-k8s-wnode-tsys9"
WNODE5_IP="100.95.201.66"
WNODE6_NAME="ultix-offstage"
WNODE6_IP="100.70.119.59"
# ultix-streaming: SSH key not yet deployed — join after setup
# WNODE7_NAME="ultix-streaming"
# WNODE7_IP="100.101.187.119"
ALL_WNODES=("$WNODE1_IP" "$WNODE2_IP" "$WNODE3_IP" "$WNODE4_IP" "$WNODE5_IP" "$WNODE6_IP")
ALL_WNODE_NAMES=("$WNODE1_NAME" "$WNODE2_NAME" "$WNODE3_NAME" "$WNODE4_NAME" "$WNODE5_NAME" "$WNODE6_NAME")
# --- SSH ---
SSH_USER="localuser"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
@@ -70,5 +43,5 @@ cn() {
# Helper: run a heredoc script on a node
cn_file() {
local ip="$1"
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${ip}" "sudo -n bash -s"
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${ip}" "sudo -n bash -s"
}
+2 -8
View File
@@ -35,7 +35,6 @@ echo "============================================"
echo ""
echo "--- [1/5] Installing bootstrap node: $CNODE1_NAME ($CNODE1_IP) ---"
# shellcheck disable=SC2087 # heredoc intentionally expands local config (node IPs, k3s version) before sending to remote
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${CNODE1_IP}" "sudo -n bash -s" <<REMOTE_BOOT
set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION"
@@ -48,9 +47,7 @@ curl -sfL https://get.k3s.io | sh -s - server \
$tls_san_flags \
--flannel-backend=vxlan \
--etcd-snapshot-schedule-cron='0 */6 * * *' \
--egress-selector-mode=agent \
--etcd-arg heartbeat-interval=1000 \
--etcd-arg election-timeout=5000
--egress-selector-mode=agent
REMOTE_BOOT
echo " cnode1 install submitted."
@@ -106,7 +103,6 @@ for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
echo ""
echo "--- [4/5] Joining server: $node_name ($node_ip) ---"
# shellcheck disable=SC2087 # heredoc intentionally expands local config before sending to remote
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN
set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION"
@@ -119,9 +115,7 @@ curl -sfL https://get.k3s.io | sh -s - server \
--advertise-address=$node_ip \
$tls_san_flags \
--flannel-backend=vxlan \
--egress-selector-mode=agent \
--etcd-arg heartbeat-interval=1000 \
--etcd-arg election-timeout=5000
--egress-selector-mode=agent
REMOTE_JOIN
echo " $node_name install submitted."
-1
View File
@@ -71,7 +71,6 @@ for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
echo ""
echo "--- [3/4] Joining server: $node_name ($node_ip) ---"
# shellcheck disable=SC2087 # heredoc intentionally expands local config before sending to remote
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN
set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION"
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/bash
#
# k8s/join-workers.sh — join worker nodes to the k3s cluster
#
# Joins all worker nodes defined in env.sh as k3s agents. Worker nodes
# run user workloads; control plane nodes are tainted NoSchedule.
#
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./env.sh
source "$SCRIPT_DIR/env.sh"
echo "============================================"
echo " Joining worker nodes to cluster"
echo " Server: $BOOTSTRAP_NAME ($BOOTSTRAP_IP)"
echo " Workers: ${#ALL_WNODE_NAMES[@]}"
echo "============================================"
# -------------------------------------------------------
# 1. Fetch join token from bootstrap node
# -------------------------------------------------------
echo ""
echo "--- [1/3] Fetching join token from $BOOTSTRAP_NAME ---"
JOIN_TOKEN=$(cn "$BOOTSTRAP_IP" 'cat /var/lib/rancher/k3s/server/token')
if [ -z "$JOIN_TOKEN" ] || [[ "$JOIN_TOKEN" == cat:* ]]; then
echo "FATAL: could not fetch token. Got: ${JOIN_TOKEN:0:40}"
exit 1
fi
echo " Token OK (masked: ${JOIN_TOKEN:0:12}***)"
SERVER_URL="https://${BOOTSTRAP_IP}:${K3S_API_PORT}"
# -------------------------------------------------------
# 2. Install k3s-agent on each worker
# -------------------------------------------------------
for i in "${!ALL_WNODES[@]}"; do
node_ip="${ALL_WNODES[$i]}"
node_name="${ALL_WNODE_NAMES[$i]}"
echo ""
echo "--- [2/3] Joining worker: $node_name ($node_ip) ---"
# Wipe any existing k3s first
cn "$node_ip" '
systemctl stop k3s-agent 2>/dev/null || true
if [ -x /usr/local/bin/k3s-agent-uninstall.sh ]; then
/usr/local/bin/k3s-agent-uninstall.sh
fi
rm -rf /etc/rancher/k3s /var/lib/rancher/k3s /var/lib/kubelet /var/lib/cni
rm -f /etc/systemd/system/k3s-agent.service
systemctl daemon-reload
ip link delete cni0 2>/dev/null || true
ip link delete flannel.1 2>/dev/null || true
' 2>/dev/null || true
# Install as agent
# shellcheck disable=SC2087 # heredoc intentionally expands local config
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_AGENT
set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION"
export K3S_URL="$SERVER_URL"
export K3S_TOKEN="$JOIN_TOKEN"
export K3S_NODE_NAME="$node_name"
curl -sfL https://get.k3s.io | sh -s - agent \
--node-name=$node_name \
--node-ip=$node_ip
REMOTE_AGENT
echo " $node_name agent install submitted."
done
# -------------------------------------------------------
# 3. Wait for all workers to appear Ready
# -------------------------------------------------------
TOTAL_NODES=$(( ${#ALL_CNODES[@]} + ${#ALL_WNODES[@]} ))
echo ""
echo "--- [3/3] Waiting for all $TOTAL_NODES nodes (${#ALL_CNODES[@]} cp + ${#ALL_WNODES[@]} workers) ---"
for i in $(seq 1 60); do
READY_NODES=$(cn "$BOOTSTRAP_IP" 'k3s kubectl get nodes --no-headers 2>/dev/null | grep -c " Ready"' 2>/dev/null || echo 0)
if [ "$READY_NODES" -ge "$TOTAL_NODES" ]; then
echo " All $TOTAL_NODES nodes Ready."
break
fi
echo " ...waiting ($i/60, $READY_NODES/$TOTAL_NODES ready)"
sleep 10
done
echo ""
echo "============================================"
echo " Node status:"
echo "============================================"
cn "$BOOTSTRAP_IP" 'k3s kubectl get nodes -o wide'
if [ "$READY_NODES" -ge "$TOTAL_NODES" ]; then
echo ""
echo "============================================"
echo " All workers joined. Cluster fully operational."
echo "============================================"
else
echo ""
echo "WARN: $READY_NODES/$TOTAL_NODES ready. Check failing nodes."
exit 1
fi
+2 -12
View File
@@ -66,20 +66,10 @@ echo " export KUBECONFIG=$KUBECONFIG_FILE"
echo " kubectl get nodes"
# -------------------------------------------------------
# 3. Deploy tuned (network-latency profile) on all cnodes
# 3. Verify cluster health
# -------------------------------------------------------
echo ""
echo "--- [3/4] Deploying tuned (network-latency) on cnodes ---"
for ip in "${ALL_CNODES[@]}"; do
echo " $ip..."
cn "$ip" 'DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq tuned 2>/dev/null; tuned-adm profile network-latency; systemctl enable tuned; systemctl restart tuned; tuned-adm active' 2>&1 | tail -1
done
# -------------------------------------------------------
# 4. Verify cluster health
# -------------------------------------------------------
echo ""
echo "--- [4/4] Verifying cluster health ---"
echo "--- [3/3] Verifying cluster health ---"
export KUBECONFIG="$KUBECONFIG_FILE"
+1 -1
View File
@@ -99,7 +99,7 @@ echo "--- Workload isolation ---"
USER_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | wc -w)
# Subtract system pods
SYSTEM_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -l k8s-app --no-headers 2>/dev/null | wc -l)
if [ "$((USER_PODS - SYSTEM_PODS))" -le 0 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi
if [ "$USER_PODS" -le 10 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi
echo ""
echo "============================================"
-1
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env bash
# shellcheck disable=SC2012 # diagnostic baseline script; ls -la listings are intentional
# baseline.sh — quick read-only baseline of a target node.
set -u
hdr() { printf '\n=== %s ===\n' "$1"; }
-2
View File
@@ -37,11 +37,9 @@ for f in setupVars.conf pihole-FTL.conf adlists.list custom.list local.list rege
echo "--- /etc/pihole/$f ---"; $DG exec pihole cat "/etc/pihole/$f" 2>&1 || true
done
echo "-- /etc/dnsmasq.d/* --"
# shellcheck disable=SC2016 # $f/$t expand inside the container's sh, not locally — single quotes are intentional
$DG exec pihole sh -c 'for f in /etc/dnsmasq.d/*; do echo "--- $f ---"; cat "$f"; done' 2>&1 || true
echo "-- pihole version --"; $DG exec pihole pihole -v 2>&1 || true
echo "-- gravity row counts --"
# shellcheck disable=SC2016 # $t expands inside the container's sh, not locally
$DG exec pihole sh -c 'for t in adlist domainlist client "group" info; do printf "%s=" "$t"; sqlite3 /etc/pihole/gravity.db "SELECT COUNT(*) FROM $t;" 2>/dev/null; done' 2>&1 || true
echo "-- adlist addresses --"
$DG exec pihole sqlite3 /etc/pihole/gravity.db "SELECT address,enabled,comment FROM adlist;" 2>&1 || true
-10
View File
@@ -1,10 +0,0 @@
# netinfra/dhcp-migration.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **DHCP migration to netinfra-01/02**
>
> **Read it here:** https://community.turnsys.com/t/306
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-323
View File
@@ -1,323 +0,0 @@
# dhcpd.conf — pfv-netinfra-01 (PRIMARY)
# Migrated from pfv-netboot 2026-07-29
# Managed via Webmin DHCP module
#
# FAILOVER: this node is PRIMARY; peer is pfv-netinfra-02 (192.168.3.253)
# Global defaults
option domain-name "knel.net";
option domain-name-servers 192.168.3.252, 192.168.3.253;
option ntp-servers 192.168.3.252, 192.168.3.253;
default-lease-time 600;
max-lease-time 7200;
ddns-update-style none;
authoritative;
# ----- failover peer (PRIMARY) -----
failover peer "pfv-dhcp" {
primary;
address 192.168.3.252;
port 647;
peer address 192.168.3.253;
peer port 647;
max-response-delay 30;
max-unacked-updates 10;
mclt 600;
split 128;
load balance max seconds 3;
}
# ----- subnet (shared /22) -----
subnet 192.168.0.0 netmask 255.255.252.0 {
option routers 192.168.3.254;
option domain-name-servers 192.168.3.252, 192.168.3.253;
option ntp-servers 192.168.3.252, 192.168.3.253;
option domain-name "knel.net";
authoritative;
allow unknown-clients;
pool {
failover peer "pfv-dhcp";
range 192.168.0.1 192.168.3.200;
}
# ---- host reservations (fixed-address; not subject to failover pool) ----
host pfv-r3-tor-mgmt-01 {
hardware ethernet 00:14:22:69:1c:37;
fixed-address 192.168.0.7;
}
host pfv-r3-tor-stor-01 {
hardware ethernet 00:13:72:46:95:e4;
fixed-address 192.168.0.9;
}
host pfv-printer {
hardware ethernet 40:9f:38:b0:b5:2f;
fixed-address 192.168.1.84;
}
host pfv-r2-tor-01 {
hardware ethernet 00:0d:56:41:7a:4d;
fixed-address 192.168.0.10;
}
host pfv-r5-core-01 {
hardware ethernet a4:ba:db:6f:ce:28;
fixed-address 192.168.0.12;
}
host upstairs-receiver {
hardware ethernet 74:5e:1c:76:e2:60;
fixed-address 192.168.0.21;
}
host ap-TableMount {
hardware ethernet e0:63:da:36:73:39;
fixed-address 192.168.3.54;
}
host AP-WallMount {
hardware ethernet e0:63:da:33:bb:1d;
fixed-address 192.168.1.182;
}
host pfv-consrv {
hardware ethernet 00:60:2e:01:50:aa;
fixed-address 192.168.3.56;
}
host garagepdu {
hardware ethernet 00:c0:b7:7e:49:78;
fixed-address 192.168.3.18;
}
host pfv-dvr {
hardware ethernet 54:2b:57:37:a7:d9;
fixed-address 192.168.3.84;
}
host appletv-livingroom {
hardware ethernet d0:d2:b0:97:81:c2;
fixed-address 192.168.1.81;
}
host pfv-stor1 {
hardware ethernet 00:00:c0:34:0c:dc;
fixed-address 192.168.1.166;
}
host 3dscan {
hardware ethernet b8:27:eb:91:31:82;
fixed-address 192.168.0.4;
}
host pfv-jetson-nano-1 {
hardware ethernet 00:04:4b:e4:17:7b;
fixed-address 192.168.3.186;
}
host tsys7-oob {
hardware ethernet f8:bc:12:35:1e:c6;
fixed-address 192.168.3.197;
}
host tsys6-oob {
hardware ethernet a4:ba:db:0b:df:a0;
fixed-address 192.168.3.196;
}
host tsys-siem {
hardware ethernet 00:15:5d:64:e8:33;
fixed-address 192.168.3.81;
}
host brother-label-printer {
hardware ethernet 04:fe:a1:56:72:e2;
fixed-address 192.168.3.52;
}
host pfv-bms {
hardware ethernet 02:5A:39:38:3E:9F;
fixed-address 192.168.3.12;
}
host stl-canon-scanner-artroom {
hardware ethernet 74:38:b7:24:fa:4e;
fixed-address 192.168.3.142;
}
host tsys-ucs-01 {
hardware ethernet bc:24:11:86:ea:1a;
fixed-address 192.168.2.51;
}
host tsys-ucs-02 {
hardware ethernet bc:24:11:c8:da:34;
fixed-address 192.168.2.54;
}
host dell-openmanage-enterprise {
hardware ethernet bc:24:11:ac:f4:6b;
fixed-address 192.168.2.113;
}
host pfv-rrinfra-rtr {
hardware ethernet 00:1d:70:0b:4f:41;
fixed-address 192.168.3.94;
}
host pfv-tsys1 {
hardware ethernet 34:17:eb:b3:b1:2d;
fixed-address 192.168.3.11;
}
host pfv-tsys2 {
hardware ethernet 18:fd:cb:00:d2:ca;
fixed-address 192.168.2.3;
}
host pfv-tsys3 {
hardware ethernet a4:4c:c8:08:d1:b8;
fixed-address 192.168.2.5;
}
host pfv-tsys4 {
hardware ethernet 98:90:96:c4:96:9a;
fixed-address 192.168.3.191;
}
host pfv-tsys5 {
hardware ethernet 18:03:73:43:ce:de;
fixed-address 192.168.0.20;
}
host pfv-tsys6 {
hardware ethernet 00:21:9b:a2:7c:53;
fixed-address 192.168.3.169;
}
host pfv-tsys7 {
hardware ethernet f8:bc:12:34:e0:74;
fixed-address 192.168.0.250;
}
host pfv-tsys9 {
hardware ethernet a4:bb:6d:e3:56:86;
fixed-address 192.168.3.58;
}
# umbrel
host tsys-umbrel {
hardware ethernet 02:2E:FF:8E:A2:D2;
fixed-address 192.168.1.97;
}
# ultix-streaming
host ultix-streaming {
hardware ethernet bc:24:11:1a:8f:6f;
fixed-address 192.168.3.78;
}
# ultix-offstage
host ultix-offstge {
hardware ethernet bc:24:11:1f:9d:83;
fixed-address 192.168.3.79;
}
# ultix-highside
host ultix-highside {
hardware ethernet a0:4a:5e:ca:46:f3;
fixed-address 192.168.3.32;
}
# pfv-k8s-cnode1
host pfv-k8s-cnode1 {
hardware ethernet bc:24:11:cb:97:10;
fixed-address 192.168.1.91;
}
# pfv-k8s-cnode2
host pfv-k8s-cnode2 {
hardware ethernet bc:24:11:40:25:f8;
fixed-address 192.168.3.113;
}
# pfv-k8s-cnode3
host pfv-k8s-cnode3 {
hardware ethernet bc:24:11:38:c0:58;
fixed-address 192.168.1.228;
}
# devbox-cloudron
host devbox-cloudron {
hardware ethernet bc:24:11:f7:b1:07;
fixed-address 192.168.1.6;
}
# hfnoc-uisp
host hfnoc-uisp {
hardware ethernet bc:24:11:a3:87:61;
fixed-address 192.168.3.193;
}
# kali-rd
host kali-rd {
hardware ethernet bc:24:11:9e:1c:e9;
fixed-address 192.168.2.37;
}
# kali-tsys
host kali-tsys {
hardware ethernet bc:24:11:16:22:d4;
fixed-address 192.168.1.114;
}
}
# ---- host declarations outside subnet (global scope, same as netboot) ----
host pfv-r6-mgmt-01 {
hardware ethernet 00:14:22:69:18:a7;
fixed-address 192.168.0.8;
}
host pfv-r1-tor-top {
hardware ethernet 00:23:ae:c1:ad:e8;
fixed-address 192.168.0.11;
}
# --- VM DHCP reservations (generated 2026-08-11, ticket #420) ---
# All pinned to current ARP-observed IPs. No forward DNS needed
# (forward records point to Tailscale 100.x addresses).
host tsys-ca {
hardware ethernet bc:24:11:32:d0:36;
fixed-address 192.168.1.181;
}
host pfv-netinfra-01 {
hardware ethernet bc:24:11:65:b2:ac;
fixed-address 192.168.3.252;
}
host pfv-netinfra-02 {
hardware ethernet bc:24:11:e4:37:53;
fixed-address 192.168.3.253;
}
host tsys-librenms {
hardware ethernet bc:24:11:5c:96:1e;
fixed-address 192.168.3.176;
}
host tsys-proxmox-datacenter {
hardware ethernet bc:24:11:e6:03:2d;
fixed-address 192.168.2.44;
}
host pfv-k8s-wnode-tsys3 {
hardware ethernet bc:24:11:ee:7e:7b;
fixed-address 192.168.1.98;
}
host pfv-proxmox-backup-server {
hardware ethernet bc:24:11:6e:12:69;
fixed-address 192.168.2.193;
}
host pfv-k8s-wnode-tsys5 {
hardware ethernet bc:24:11:c7:a8:6c;
fixed-address 192.168.1.5;
}
host preprod-hfnoc-uisp {
hardware ethernet bc:24:11:74:d6:8a;
fixed-address 192.168.3.192;
}
host tsys-awx {
hardware ethernet bc:24:11:80:0d:16;
fixed-address 192.168.3.115;
}
host pfv-rr-middleware-02 {
hardware ethernet bc:24:11:96:0e:ee;
fixed-address 192.168.1.117;
}
host tsys-proxmox-mailgw-01 {
hardware ethernet bc:24:11:56:61:18;
fixed-address 192.168.1.11;
}
host pfv-k8s-wnode-tsys7 {
hardware ethernet bc:24:11:30:b8:07;
fixed-address 192.168.1.109;
}
host pfv-rr-middleware-01 {
hardware ethernet bc:24:11:1e:61:cf;
fixed-address 192.168.1.110;
}
host tsys-voip {
hardware ethernet bc:24:11:23:ce:04;
fixed-address 192.168.1.70;
}
host tsys-proxmox-mailgw-02 {
hardware ethernet bc:24:11:5f:e5:2c;
fixed-address 192.168.1.10;
}
host pfv-k8s-wnode-tsys6 {
hardware ethernet bc:24:11:fa:6e:b5;
fixed-address 192.168.1.111;
}
host tsys-siem-new {
hardware ethernet bc:24:11:ee:67:e2;
fixed-address 192.168.1.223;
}
-193
View File
@@ -1,193 +0,0 @@
# dhcpd.conf — pfv-netinfra-02 (SECONDARY)
# Migrated from pfv-netboot 2026-07-29
# Managed via Webmin DHCP module
#
# FAILOVER: this node is SECONDARY; peer is pfv-netinfra-01 (192.168.3.252)
# Global defaults
option domain-name "knel.net";
option domain-name-servers 192.168.3.252, 192.168.3.253;
option ntp-servers 192.168.3.252, 192.168.3.253;
default-lease-time 600;
max-lease-time 7200;
ddns-update-style none;
authoritative;
# ----- failover peer (SECONDARY) -----
failover peer "pfv-dhcp" {
secondary;
address 192.168.3.253;
port 647;
peer address 192.168.3.252;
peer port 647;
max-response-delay 30;
max-unacked-updates 10;
load balance max seconds 3;
}
# ----- subnet (shared /22) -----
subnet 192.168.0.0 netmask 255.255.252.0 {
option routers 192.168.3.254;
option domain-name-servers 192.168.3.252, 192.168.3.253;
option ntp-servers 192.168.3.252, 192.168.3.253;
option domain-name "knel.net";
authoritative;
allow unknown-clients;
pool {
failover peer "pfv-dhcp";
range 192.168.0.1 192.168.3.200;
}
# ---- host reservations (identical to primary) ----
host pfv-r3-mgmt {
hardware ethernet 00:14:22:69:1c:37;
fixed-address 192.168.0.7;
}
host pfv-r3-stor {
hardware ethernet 00:13:72:46:95:e4;
fixed-address 192.168.0.9;
}
host pfv-printer {
hardware ethernet 40:9f:38:b0:b5:2f;
fixed-address 192.168.1.84;
}
host pfv-r2-tor1 {
hardware ethernet 00:0d:56:41:7a:4d;
fixed-address 192.168.0.10;
}
host pfv-r5-core-01 {
hardware ethernet a4:ba:db:6f:ce:28;
fixed-address 192.168.0.12;
}
host upstairs-receiver {
hardware ethernet 74:5e:1c:76:e2:60;
fixed-address 192.168.0.21;
}
host ap-TableMount {
hardware ethernet e0:63:da:36:73:39;
fixed-address 192.168.3.54;
}
host AP-WallMount {
hardware ethernet e0:63:da:33:bb:1d;
fixed-address 192.168.1.182;
}
host pfv-consrv {
hardware ethernet 00:60:2e:01:50:aa;
fixed-address 192.168.3.56;
}
host garagepdu {
hardware ethernet 00:c0:b7:7e:49:78;
fixed-address 192.168.3.18;
}
host pfv-dvr {
hardware ethernet 54:2b:57:37:a7:d9;
fixed-address 192.168.3.84;
}
host appletv-livingroom {
hardware ethernet d0:d2:b0:97:81:c2;
fixed-address 192.168.1.81;
}
host pfv-stor1 {
hardware ethernet 00:00:c0:34:0c:dc;
fixed-address 192.168.1.166;
}
host 3dscan {
hardware ethernet b8:27:eb:91:31:82;
fixed-address 192.168.0.4;
}
host pfv-jetson-nano-1 {
hardware ethernet 00:04:4b:e4:17:7b;
fixed-address 192.168.3.186;
}
host tsys7-oob {
hardware ethernet f8:bc:12:35:1e:c6;
fixed-address 192.168.3.197;
}
host tsys6-oob {
hardware ethernet a4:ba:db:0b:df:a0;
fixed-address 192.168.3.196;
}
host tsys-siem {
hardware ethernet 00:15:5d:64:e8:33;
fixed-address 192.168.3.81;
}
host brother-label-printer {
hardware ethernet 04:fe:a1:56:72:e2;
fixed-address 192.168.3.52;
}
host pfv-bms {
hardware ethernet 02:5A:39:38:3E:9F;
fixed-address 192.168.3.12;
}
host stl-canon-scanner-artroom {
hardware ethernet 74:38:b7:24:fa:4e;
fixed-address 192.168.3.142;
}
host tsys-ucs-01 {
hardware ethernet bc:24:11:86:ea:1a;
fixed-address 192.168.2.51;
}
host tsys-ucs-02 {
hardware ethernet bc:24:11:c8:da:34;
fixed-address 192.168.2.54;
}
host dell-openmanage-enterprise {
hardware ethernet bc:24:11:ac:f4:6b;
fixed-address 192.168.2.113;
}
host pfv-rrinfra-rtr {
hardware ethernet 00:1d:70:0b:4f:41;
fixed-address 192.168.3.94;
}
host pfv-tsys1 {
hardware ethernet 34:17:eb:b3:b1:2d;
fixed-address 192.168.3.11;
}
host pfv-tsys2 {
hardware ethernet 18:fd:cb:00:d2:ca;
fixed-address 192.168.2.3;
}
host pfv-tsys3 {
hardware ethernet a4:4c:c8:08:d1:b8;
fixed-address 192.168.2.5;
}
host pfv-tsys4 {
hardware ethernet 98:90:96:c4:96:9a;
fixed-address 192.168.3.191;
}
host pfv-tsys5 {
hardware ethernet 18:03:73:43:ce:de;
fixed-address 192.168.0.20;
}
host pfv-tsys6 {
hardware ethernet 00:21:9b:a2:7c:53;
fixed-address 192.168.3.169;
}
host pfv-tsys7 {
hardware ethernet f8:bc:12:34:e0:74;
fixed-address 192.168.0.250;
}
host pfv-tsys9 {
hardware ethernet a4:bb:6d:e3:56:86;
fixed-address 192.168.3.58;
}
}
# ---- host declarations outside subnet (global scope, same as netboot) ----
host subodev-torsw01 {
hardware ethernet 00:14:22:69:18:a7;
fixed-address 192.168.0.8;
}
host pfv-r1-tor-top {
hardware ethernet 00:23:ae:c1:ad:e8;
fixed-address 192.168.0.11;
}
host tailscale-router {
hardware ethernet bc:24:11:8a:69:04;
fixed-address 192.168.3.16;
}
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
# install-dhcp.sh — installs isc-dhcp-server + Webmin on a netinfra node.
# Does NOT start the DHCP service. Run on the target node itself.
set -e
echo "=== Installing isc-dhcp-server ==="
apt-get update -qq
apt-get install -y isc-dhcp-server
echo "=== Installing Webmin ==="
if ! dpkg -l | grep -q '^ii.*webmin'; then
curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/webmin-setup-repo.sh -o /tmp/webmin-setup.sh
sh /tmp/webmin-setup.sh -f
rm -f /tmp/webmin-setup.sh
apt-get install -y webmin
else
echo "Webmin already installed"
fi
echo "=== Writing /etc/default/isc-dhcp-server ==="
cat > /etc/default/isc-dhcp-server <<'EOF'
# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server)
INTERFACESv4="ens18"
INTERFACESv6=""
EOF
echo "=== Stopping DHCP service (should not serve yet) ==="
systemctl stop isc-dhcp-server 2>/dev/null || true
systemctl disable isc-dhcp-server 2>/dev/null || true
echo "=== Done. DHCP installed but NOT started. ==="
-10
View File
@@ -1,10 +0,0 @@
# netinfra/dns-cluster-setup/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Technitium DNS cluster setup**
>
> **Read it here:** https://community.turnsys.com/t/306
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
+1 -1
View File
@@ -44,7 +44,7 @@ if command -v sqlite3 >/dev/null 2>&1; then
echo "-- adlist count --"; sqlite3 -readonly "$G" "SELECT COUNT(*) FROM adlist;" 2>&1
echo "-- domainlist count by type --"; sqlite3 -readonly "$G" "SELECT type,COUNT(*) FROM domainlist GROUP BY type;" 2>&1
echo "-- domainlist (allow=0/allow_exact, deny=1/deny_exact, etc.) first 80 --"; sqlite3 -readonly "$G" "SELECT type,domain,enabled,comment FROM domainlist LIMIT 80;" 2>&1
echo "-- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1
echo -- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1
echo "-- group --"; sqlite3 -readonly "$G" "SELECT id,name,enabled,comment FROM 'group';" 2>&1
echo "-- info --"; sqlite3 -readonly "$G" "SELECT * FROM info;" 2>&1
else
+2 -2
View File
@@ -28,7 +28,7 @@ services:
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones, e.g:
TZ: 'America/Chicago'
# Set a password to access the web interface. Not setting one will result in a random password being assigned
FTLCONF_webserver_api_password: 'REDACTED_PASSWORD'
FTLCONF_webserver_api_password: 'Gransyan1!'
# If using Docker's default `bridge` network setting the dns listening mode should be set to 'all'
FTLCONF_dns_listeningMode: 'all'
# Volumes store your data between container upgrades
@@ -326,7 +326,7 @@ tsys-ntp dockurr/chrony Up 7 days (healthy) WDIR=/root/NTP
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"FTLCONF_webserver_api_password=REDACTED_PASSWORD",
"FTLCONF_webserver_api_password=Gransyan1!",
"FTLCONF_dns_listeningMode=all",
"TZ=America/Chicago",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+115 -9
View File
@@ -1,10 +1,116 @@
# netinfra/pfv-netboot-setup.md
# pfv-netboot — Reference Network Infrastructure (READ-ONLY reference)
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **pfv-netboot reference node setup**
>
> **Read it here:** https://community.turnsys.com/t/306
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
> **Status:** REFERENCE SOURCE ONLY. This node is production infrastructure.
> Do **not** modify it. This document describes it as audited so its services can
> be replicated to `pfv-netinfra-01` / `pfv-netinfra-02`. All data below was
> collected by **read-only** audit scripts (`audit-netboot.sh`,
> `deep-audit-netboot.sh`, `gather-configs.sh`) on 2026-07-27/28.
## 1. Host
| Item | Value |
|---|---|
| Hostname / FQDN | `pfv-netboot` / `pfv-netboot.knel.net` |
| OS | Debian GNU/Linux 12 (bookworm), kernel 6.1.0-44-amd64 |
| Hardware | 2 vCPU, ~1.9 GiB RAM, 491 GB disk (18 GB used) |
| Timezone | `America/Chicago` (US/Central) |
| LAN | `eth0` static `192.168.3.250/22`, gw `192.168.3.254` (`/etc/network/interfaces`) |
| Tailscale | `100.103.64.82` (`tailscale0`) |
| DNS resolver | Tailscale MagicDNS — `/etc/resolv.conf``100.100.100.100` |
| Docker | Docker Engine 29.6.2 (containerd v2.2.6, runc 1.3.6) |
| Access | `localuser` has passwordless sudo; **not** in `docker` group (uses `sudo docker`) |
`eth1` is up but unconfigured; many docker bridges exist (`pihole_default`,
`ntp_default`, `dns_default`, and several stale ones).
## 2. Services overview
| Service | Form | Running? |
|---|---|---|
| **Pi-hole** (DNS sinkhole, recursive resolver) | Docker container `pihole` | ✅ healthy |
| **NTP** — overlay on Tailscale IP | Docker container `tsys-ntp` (`dockurr/chrony`) | ✅ healthy |
| **NTP** — system clock + LAN serving | bare-metal `ntpsec` (`ntpd`) | ✅ active, enabled |
| **Technitium DNS** (authoritative for `knel.net`) | Docker container | ❌ **not running**; config preserved in orphaned volume |
## 3. Pi-hole (container)
- **Compose:** `/root/pihole/docker-compose.yml` (compose project `pihole`)
- **Image:** `pihole/pihole:latest` — Core **v6.1.2**, Web v6.2.1, FTL v6.2.2
- **Container:** `pihole`, `restart: always`, `cap_add: [SYS_NICE]`, network `pihole_default`
- **Ports (host):**
| Host | Container | Purpose |
|---|---|---|
| `53/tcp`, `53/udp` | 53 | DNS |
| `10002/tcp` | 80 | Web admin (HTTP) |
| `10003/tcp` | 443 | Web admin (HTTPS, self-signed) |
- **Environment:** `TZ=America/Chicago`, `FTLCONF_webserver_api_password=Gransyan1!`, `FTLCONF_dns_listeningMode=all`
- **Data:** bind mount `/root/pihole/etc-pihole:/etc/pihole` (dir owned by `localuser`; files by container `pihole` uid)
- **Config (Pi-hole v6 TOML):** `pihole.toml`. Key settings:
- Upstream DNS: `192.168.3.16`, `8.8.8.8`, `2001:4860:4860::8888`
- `listeningMode = "ALL"`, `interface = "eth0"`, `dns.port = 53`, `dns.domain = "lan"`
- `queryLogging = true`, DNSSEC off
- **Adlists:** one entry — `https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts` (in `gravity.db`/`adlists.list`)
- **Gravity DB:** `/etc/pihole/gravity.db` (~5.5 MB) holds adlists/domainlists/clients/groups
- **Web admin:** `http://pfv-netboot:10002/admin/` (password `Gransyan1!`)
- Note: query history `pihole-FTL.db` (~2.5 GB) is transient and **excluded** from replication.
## 4. NTP (two layers)
### 4a. chrony container (`tsys-ntp`) — overlay on the Tailscale IP
- **Compose:** `/root/NTP/docker-compose.yml` (project `ntp`)
- **Image:** `dockurr/chrony`
- **Env:** `NTP_SERVERS=pool.ntp.org`
- **Ports:** `100.103.64.82:123:123/udp` — bound specifically to the **Tailscale IP**
- `restart: always`
- chrony.conf (generated): `server pool.ntp.org iburst`, `allow all`, `rtcsync`
- On netboot this coexists with bare-metal ntpsec because ntpsec here does **not** pre-bind the specific Tailscale-IP socket, letting Docker claim it.
### 4b. bare-metal `ntpsec`
- Unit `ntpsec.service` — active, enabled; `/usr/sbin/ntpd -c /etc/ntpsec/ntp.conf -g -N -u ntpsec:ntpsec`
- **Config** (`/etc/ntpsec/ntp.conf`):
```
driftfile /var/lib/ntp/ntp.drift
leapfile /usr/share/zoneinfo/leap-seconds.list
server pfvsvrpi.knel.net
restrict 127.0.0.1
restrict ::1
```
- Listens on all local addresses (incl. Tailscale) for UDP/123; serves LAN clients.
## 5. Technitium DNS (currently stopped)
- **Not running** — no container and **no compose file** exists for it.
- A previous deployment left an **orphaned Docker volume** `dns_tsys-dns-config`
(mountpoint `/var/lib/docker/volumes/dns_tsys-dns-config/_data`) whose contents
are intact (last activity 2025-06-23). A second typo'd volume
`dns_tyss-dns-config` is empty.
- Config files are **binary** (Technitium's own serialization), but copy verbatim:
`dns.config`, `auth.config`, `log.config`, `scopes/Default.scope`,
`self-signed-cert.pfx`, `cache.bin`, `zones/`, `stats/`, `logs/`.
- **Zones present** (12 reverse + 1 forward):
- `knel.net.zone` — forward zone; SOA `dns.knel.net. hostadmin.knel.net.` (serial `2025062313`). A-records for the internal fleet, including: `tsys1`, `rr-middleware`, `pfv-netboot`, `pfv-k8s-cnode1`…`cnode5`, `pfv-k8s-wnode3`, `tsys-k8scloud-netcup-1`, `tsys-kali-vptechops`, `tsys-kali-dev`; NS `dns.knel.net`.
- Reverse zones for Tailscale CGNAT ranges (`100.x.in-addr.arpa`): `199.86`, `145.105`, `181.103`, `184.108`, `194.67`, `2.108`, `211.114`, `46.96`, `64.103`, `75.110`, `97.82`, `119.127`.
- **Auth:** `auth.config` defines user `admin` (Administrators group) with a stored password hash; the plaintext password is whatever was set on the original Technitium instance.
- The compose project name historically was `dns` (network `dns_default` still exists).
## 6. Firewall / misc
- nftables/iptables: mostly Docker + Tailscale chains (`ts-input`, `ts-forward`,
`DOCKER`, `DOCKER-FORWARD`); default `INPUT ACCEPT`, `FORWARD DROP`,
`OUTPUT ACCEPT`. No UFW / firewalld.
- Also runs (out of scope for this replication): Samba (137/138/139, 445), NFS
(2049), rpcbind (111), Postfix (25), Cockpit (9090), Beszel agent, webmin/
usermin (10000/10002/20000), Tailscale (41641).
- SELinux absent; AppArmor default docker profile.
## 7. How it was audited (no changes made)
```bash
ssh localuser@pfv-netboot 'bash -s' < audit-netboot.sh # broad read-only sweep
ssh localuser@pfv-netboot 'bash -s' < deep-audit-netboot.sh # docker inspect + compose
ssh localuser@pfv-netboot 'bash -s' < gather-configs.sh # pihole.toml + technitium
```
Artifacts: `netboot-audit.txt`, `netboot-deep-audit.txt`, `netboot-configs.txt`.
+219 -9
View File
@@ -1,10 +1,220 @@
# netinfra/pfv-netinfra-setup.md
# pfv-netinfra-01 / pfv-netinfra-02 — Network Services Setup
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **pfv-netinfra-01/02 initial setup**
>
> **Read it here:** https://community.turnsys.com/t/306
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
These two nodes replicate the network-infrastructure services of **pfv-netboot**
(Pi-hole, Technitium DNS, NTP). They were deployed by `setup-netinfra.sh`, which
reads config from pfv-netboot (read-only) and relays it to each target.
## 1. Nodes
| | pfv-netinfra-01 | pfv-netinfra-02 |
|---|---|---|
| OS | Debian 13 (trixie), kernel 6.12.96+deb13 | Debian 13 (trixie) |
| LAN | `ens18` `192.168.3.252/24` | `ens18` `192.168.3.253/24` |
| Tailscale | `100.70.181.72` | `100.93.194.82` |
| RAM / Disk | 1.9 GiB / 30 GB (27 GB free) | 3.7 GiB / 30 GB (27 GB free) |
| Resolver | Tailscale MagicDNS (`100.100.100.100`) | same |
| Docker | 29.6.2 (pre-installed, enabled) | 29.6.2 |
| Access | `ssh localuser@pfv-netinfra-0X`, passwordless sudo; `localuser` **not** in docker group → use `sudo docker` | same |
## 2. Service layout
All services live under `/home/localuser/services/<svc>/` (owned by `localuser`
so the compose files are directly editable; data dirs keep container uids):
```
/home/localuser/services/
├── pihole/
│ ├── docker-compose.yml
│ └── etc-pihole/ # copied from netboot /root/pihole/etc-pihole
│ ├── pihole.toml # Pi-hole v6 config (upstreams, etc.)
│ ├── gravity.db # adlists / domainlists / clients / groups
│ ├── adlists.list
│ ├── dnsmasq.conf
│ ├── tls.{crt,pem,crt_ca}
│ └── versions
├── ntp/
│ └── docker-compose.yml # chrony container (see §5 — not used; host ntpsec serves)
└── technitium/
├── docker-compose.yml
└── config/ # copied from netboot orphaned volume dns_tsys-dns-config/_data
├── dns.config
├── auth.config
├── scopes/Default.scope
├── self-signed-cert.pfx
└── zones/ # knel.net.zone + 12 Tailscale reverse zones
```
## 3. Pi-hole (container `pihole`)
Image `pihole/pihole:latest`; `restart: always`; `cap_add: [SYS_NICE]`.
| Host port | Container | Purpose |
|---|---|---|
| `53/tcp`, `53/udp` | 53 | DNS (the LAN/Tailscale recursive resolver) |
| `10002/tcp` | 80 | Web admin (HTTP) |
| `10003/tcp` | 443 | Web admin (HTTPS) |
`docker-compose.yml`:
```yaml
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
hostname: pihole
ports:
- "53:53/tcp"
- "53:53/udp"
- "10002:80/tcp"
- "10003:443/tcp"
environment:
TZ: 'America/Chicago'
FTLCONF_webserver_api_password: 'Gransyan1!'
FTLCONF_dns_listeningMode: 'all'
volumes:
- './etc-pihole:/etc/pihole'
cap_add:
- SYS_NICE
restart: always
```
- Upstream DNS (from copied `pihole.toml`): `192.168.3.16`, `8.8.8.8`, `2001:4860:4860::8888`.
- Adlist: `https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts`.
- `pihole.toml` `interface` was adapted from netboot's `eth0` to the target's `ens18`.
- Web admin: `http://<node>:10002/admin/` — password **`Gransyan1!`** (same as netboot).
- Web UI URL per node: `http://100.70.181.72:10002/admin/` (-01), `http://100.93.194.82:10002/admin/` (-02).
## 4. Technitium DNS (container `tsys-dns`)
Image `technitium/dns-server`; `restart: always`. Authoritative DNS for
`knel.net` (and Tailscale reverse zones), config copied verbatim from netboot's
orphaned `dns_tsys-dns-config` volume.
| Host port | Container | Purpose |
|---|---|---|
| `5300/tcp`, `5300/udp` | 53 | DNS (remapped — see note) |
| `5380/tcp` | 5380 | Web console (HTTP) |
| `53443/tcp` | 53443 | Web console (HTTPS) |
`docker-compose.yml`:
```yaml
services:
technitium:
image: technitium/dns-server
container_name: tsys-dns
ports:
- "5300:53/tcp"
- "5300:53/udp"
- "5380:5380/tcp"
- "53443:53443/tcp"
volumes:
- './config:/etc/dns'
restart: always
```
- Zones loaded (verified): `knel.net` SOA → `dns.knel.net. hostadmin.knel.net. 2025062313 900 300 604800 900`, plus 12 Tailscale reverse zones.
- Web console: `http://<node>:5380/` → user **`admin`** + the original Technitium
password (carried over via `auth.config`). If the password is unknown, reset it
from the console or by removing `config/auth.config` and recreating the container.
- **Port note:** Technitium's native DNS port (53) is remapped to host **5300**
because Pi-hole already owns host :53 (they cannot both bind 0.0.0.0:53). To
query the authoritative server: `dig -p 5300 @<node> knel.net SOA`. To make
Pi-hole resolve `knel.net` via Technitium, add a conditional/local upstream in
Pi-hole pointing to the container (e.g. `127.0.0.1#5300` is not host-reachable
from Pi-hole's netns — use the docker bridge IP of `tsys-dns`, or add
`knel.net` A-records directly in Pi-hole's Local DNS).
## 5. NTP (host `ntpsec`, not a container)
Both targets **already run a bare-metal `ntpsec` daemon** (active, enabled) that
serves NTP on every local address — including the Tailscale IP — and keeps the
system clock synced. This is the **same daemon family as netboot's own bare-metal
ntpsec**.
- **Why no chrony container?** netboot's chrony container (`tsys-ntp`) binds the
Tailscale IP `100.103.64.82:123`; on netboot that works only because its ntpsec
does **not** pre-bind the specific Tailscale-IP socket. On these targets ntpsec
**does** bind the Tailscale IP, so the container cannot claim it (`address
already in use`) and would be a non-functional duplicate (verified: the
container started but never synced — Stratum 0). It is therefore intentionally
**omitted**; host ntpsec provides NTP. `setup-netinfra.sh` detects an active
host NTP unit and removes any stale `tsys-ntp` container.
- ntpsec config (`/etc/ntpsec/ntp.conf`): Debian NTP pool (`0-3.debian.pool.ntp.org`),
`restrict default kod nomodify noquery limited` (serves time, blocks mgmt queries).
- Verified sync: -01 stratum 2 (~2 ms offset), -02 stratum 3 (~0.2 ms offset),
leap normal.
The `ntp/docker-compose.yml` is still written on each node for parity/reference
(and in case the host NTP is ever disabled — then `sudo docker compose -f
/home/localuser/services/ntp/docker-compose.yml up -d` brings up chrony).
## 6. Verification results (2026-07-28)
| Check | pfv-netinfra-01 | pfv-netinfra-02 |
|---|---|---|
| `pihole` health | healthy | healthy |
| `dig @127.0.0.1:53 pi.hole` | `172.18.0.2` | `172.18.0.2` |
| Pi-hole web `:10002` | HTTP 302 (→login) | HTTP 302 |
| `dig @127.0.0.1:5300 knel.net SOA` | SOA answered | SOA answered |
| Technitium web `:5380` | HTTP 200 | HTTP 200 |
| NTP daemon | ntpsec, stratum 2, synced | ntpsec, stratum 3, synced |
## 7. Operating the services
```bash
# status
sudo docker ps
# Pi-hole
sudo docker compose -f /home/localuser/services/pihole/docker-compose.yml ps
sudo docker compose -f /home/localuser/services/pihole/docker-compose.yml logs -f
sudo docker exec pihole pihole -v # version
sudo docker exec pihole pihole -g # rebuild gravity
sudo docker exec pihole pihole -a -p # set/change web password
# Technitium
sudo docker compose -f /home/localuser/services/technitium/docker-compose.yml logs -f
sudo docker exec tsys-dns sh # explore /etc/dns
# NTP (host)
systemctl status ntpsec
ntpq -pn
```
## 8. Differences from pfv-netboot (intentional)
1. **Layout** under `/home/localuser/services/` instead of `/root` (so `localuser`
can manage compose files); Pi-hole data dir still owned by `localuser`, as on netboot.
2. **Pi-hole `interface`** set to `ens18` (targets' NIC) instead of netboot's `eth0`.
3. **NTP:** host `ntpsec` (Debian pool) used instead of netboot's chrony container
(the container cannot bind the Tailscale IP here; see §5).
4. **Technitium DNS** host port remapped `53 → 5300` to avoid clashing with Pi-hole
on `:53`. The `knel.net` zone and all reverse zones are identical to netboot's.
5. Pi-hole query logs (`pihole-FTL.db*`) and regenerable caches/backups are not
copied (transient); gravity DB and all configuration are.
## 9. Re-running / reproducing
`setup-netinfra.sh` is **idempotent** — it skips re-copying config if already
present and uses `docker compose up -d` (no-ops when unchanged). It reads
pfv-netboot read-only and never mutates it.
```bash
./setup-netinfra.sh # deploy to both nodes
./setup-netinfra.sh pfv-netinfra-01 # deploy one node
./setup-netinfra.sh pfv-netinfra-01 verify # verify only
```
Prerequisites: SSH key access to all three hosts as `localuser` with passwordless
sudo; the targets reach `192.168.3.16`/`8.8.8.8` for Pi-hole upstream and the
internet for image pulls.
## 10. Files in this directory
| File | Purpose |
|---|---|
| `setup-netinfra.sh` | orchestrator: deploys + verifies the clone on -01/-02 |
| `audit-netboot.sh` | broad read-only audit of pfv-netboot |
| `deep-audit-netboot.sh` | docker inspect / compose / volume deep audit (read-only) |
| `gather-configs.sh` | targeted config pull (pihole.toml, technitium) (read-only) |
| `baseline.sh` | read-only baseline of a target node |
| `netboot-audit.txt`, `netboot-deep-audit.txt`, `netboot-configs.txt` | audit output |
| [`pfv-netboot-setup.md`](pfv-netboot-setup.md) | reference-node documentation |
| [`pfv-netinfra-setup.md`](pfv-netinfra-setup.md) | this document |
-3
View File
@@ -1,3 +0,0 @@
# Pi-hole web UI password. NEVER commit the real .env — only this template.
# Copy to .env and set the value before `docker compose up -d`.
PIHOLE_WEB_PASSWORD=changeme
-10
View File
@@ -1,10 +0,0 @@
# netinfra/pihole/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Pi-hole recursive DNS hardening**
>
> **Read it here:** https://community.turnsys.com/t/306
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
-55
View File
@@ -1,55 +0,0 @@
services:
pihole:
container_name: pihole
# Root cause of the 2026-08 gravity.db corruption: default /dev/shm (64M)
# was too small for FTL's shared-memory metrics. 1024M has been stable.
shm_size: '1024M'
image: pihole/pihole:2026.07.0
hostname: pihole
entrypoint: ["/usr/local/bin/gravity-validate.sh"]
ports:
- "53:53/tcp"
- "53:53/udp"
- "10002:80/tcp"
- "10003:443/tcp"
environment:
TZ: 'America/Chicago'
FTLCONF_webserver_api_password: '${PIHOLE_WEB_PASSWORD}'
FTLCONF_dns_listeningMode: 'all'
# Rate-limiting disabled (count=0). Uptime Kuma on Cloudron VPS sends
# high-volume DNS queries for monitoring; default 1000/60s limit was
# causing intermittent REFUSED responses → Uptime Kuma flapping.
FTLCONF_dns_rateLimit_count: '0'
FTLCONF_dns_rateLimit_interval: '0'
FTLCONF_dns_upstreams: '["8.8.8.8"]'
volumes:
- './etc-pihole:/etc/pihole'
- './etc-dnsmasq.d:/etc/dnsmasq.d'
- './gravity-validate.sh:/usr/local/bin/gravity-validate.sh:ro'
cap_add:
- SYS_NICE
restart: always
healthcheck:
test: ["CMD-SHELL", "dig +short +norecurse @127.0.0.1 pi.hole >/dev/null 2>&1 && test -s /etc/pihole/gravity.db || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
labels:
autoheal: "true"
networks:
- default
- dnsnet
autoheal:
container_name: autoheal
image: willfarrell/autoheal:1.2.0
environment:
AUTOHEAL_CONTAINER_LABEL: autoheal
AUTOHEAL_INTERVAL: 30
AUTOHEAL_START_PERIOD: 60
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
restart: always
networks:
dnsnet:
external: true
-28
View File
@@ -1,28 +0,0 @@
#!/bin/bash
# gravity-validate.sh — pre-start integrity check for Pi-hole's gravity.db
#
# Runs as the container entrypoint. If gravity.db is empty or has an invalid
# SQLite header (the symptom of the /dev/shm corruption outage), move it aside
# so Pi-hole regenerates a clean DB on start instead of crashing.
set -e
GRAVITY_DB="/etc/pihole/gravity.db"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
if [ -f "$GRAVITY_DB" ]; then
if [ ! -s "$GRAVITY_DB" ]; then
echo "[gravity-validate] gravity.db is empty, moving aside"
mv "$GRAVITY_DB" "${GRAVITY_DB}.corrupt.${TIMESTAMP}"
else
HEADER=$(head -c 15 "$GRAVITY_DB" 2>/dev/null || true)
if [ "$HEADER" != "SQLite format 3" ]; then
echo "[gravity-validate] gravity.db invalid header, moving aside"
mv "$GRAVITY_DB" "${GRAVITY_DB}.corrupt.${TIMESTAMP}"
fi
fi
fi
# Keep only the 3 most recent corrupt backups (names carry a timestamp,
# so lexical reverse-sort = newest-first).
find /etc/pihole -maxdepth 1 -name 'gravity.db.corrupt.*' -print 2>/dev/null \
| sort -r | tail -n +4 | xargs -r rm -f
echo "[gravity-validate] OK, starting Pi-hole"
exec /usr/bin/start.sh "$@"
+2 -1
View File
@@ -34,6 +34,7 @@ set -euo pipefail
NETBOOT="localuser@pfv-netboot"
SVC_ROOT="/home/localuser/services"
PIHOLE_PW='Gransyan1!' # replicated verbatim from netboot compose
log() { printf '\n\033[1;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
warn() { printf '\n\033[1;33m[WARN %s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
@@ -116,7 +117,7 @@ services:
- "10003:443/tcp"
environment:
TZ: 'America/Chicago'
FTLCONF_webserver_api_password: 'REDACTED_PASSWORD'
FTLCONF_webserver_api_password: 'Gransyan1!'
FTLCONF_dns_listeningMode: 'all'
volumes:
- './etc-pihole:/etc/pihole'
@@ -1,25 +0,0 @@
! #369/#394: Convert cross-rack trunk ch1 from static (mode=on) to LACP
! Switch: core-sw01 (Dell PowerConnect 5448, rack 5)
! WHEN: Friday maintenance window — BOTH switches must change together
! RISK: Brief storage-net outage during transition (seconds)
!
! Current: g13-g16 in ch1, mode=on (static, no failure detection)
! Target: g13-g16 in ch1, mode=auto (LACP active partner negotiation)
!
! NOTE: core-sw01 hash stays layer-2-3 (hardware limit — best available)
! tor3-stor hash stays layer-2-3-4 (already set)
!
! IMPORTANT: Run this SIMULTANEOUSLY with tor3-stor change.
! If one side is LACP and other is static, trunk goes down until
! both sides match. Plan for ~30s storage-net outage.
!
enable
configure
interface range ethernet g13-g16
no channel-group
channel-group 1 mode auto
exit
exit
show interfaces status port-channel 1
show lacp port-channel 1
copy running-config startup-config
@@ -1,5 +0,0 @@
! pfv-r3-tor-stor-01 — MAC table + port details (Dell PowerConnect 5324 commands)
enable
show bridge addressing-table address
show interfaces description
show interfaces status
-12
View File
@@ -1,12 +0,0 @@
! pfv-r3-tor-stor-01 — Neyland 24T (Radlan-based, rack 3 storage TOR)
! Radlan CLI uses different keywords than DNOS
enable
show system
show inventory
show interfaces configuration
show interfaces description
show port-channel
show lag
show vlan database
show vlan
show running-config
@@ -1,7 +0,0 @@
! pfv-r5-core-01 — MAC address table + LLDP neighbors
terminal datadump
enable
show mac-address-table
show lldp info
show lldp neighbors
show interfaces status port-channel
-7
View File
@@ -1,7 +0,0 @@
! pfv-r5-core-01 — Dell PowerConnect 5448 (core switch, rack 5)
! Need running-config to diagnose ch1 port mismatch (g16 up but not in LAG, g17 down)
terminal datadump
enable
show running-config
show interfaces configuration
show interfaces description

Some files were not shown because too many files have changed in this diff Show More