WRKLOG-2026-06-09

Summary

Monday. Heavy documentation and operations session. Decrypted and placed 4 encrypted session captures from 06-08 into d001 (intune-cert, rsyslog commands, syslog session). Created 5 runbooks (VNC removal, rsyslog new sources, Monad pipeline, NAS resync, TCP clocks batch 2). Full BMS Controller Segmentation project migrated from Principia LaTeX (9 sections, 5 Mermaid diagrams, 4 PDFs) into d001 encrypted project with d001 open/close workflow. Git tree audited — removed misplaced files (Alexandra Angulo resume, generated PDFs, runtime lock files), created .graveyard/ for local archive. TCP clock batch 2: 9 new MACs added to ISE via ERS batch API, discovered new switch deployed without RADIUS/AAA config — clocks in ISE but can’t authenticate.

URGENT - All Domains

Carryover Backlog (CRITICAL)

Task Details Origin Days Status

MSCHAPv2 Migration Report

Report due. 6-sheet Standard Report (exec summary, trend, waves, device detail, stale, policy match). Sheet 6 added 05-14: policy match by protocol for removal planning + anonymous identity validation. Migration window 2026-05-04 to 2026-05-30. ~6,227 devices, 5 waves.

2026-04-17

57

P0 - DUE — run report this week

Abnormal Security — ✅ COMPLETE

CR-2026-05-07-abnormal-read-write. CAB approved 2026-05-12. Implemented successfully 2026-05-13. Read/write enabled for pilot group. Post-deployment validation pending.

2026-05-07

37

✅ IMPLEMENTED — post-validation pending

SIEM QRadar → Sentinel Migration

Lead role. Monad console error RESOLVED 2026-05-12 — secrets configured in CHLA production tenant. ISE secure syslog integration in progress — cert imported, remote logging target configured, streaming errors under investigation. Blocking: DCR not created (Rule ID + Stream Name). Azure private network policy unresolved. Victor + Mauricio action.

2026-04-10

64

P0 - ACTIVE — ISE syslog + DCR blocking

Monad Pipeline Evaluation

Sentinel output connector. Console error resolved. 3 of 6 values configured. Remaining: Endpoint URL (have it), Rule ID + Stream Name (need DCR). ISE Remote Logging Target configured 2026-05-18 — TLS cert imported, secure syslog target created. Streaming errors in Monad console under investigation.

2026-03-11

94

P0 - ACTIVE — ISE integration in progress

Guest Redirect ACL

Guest redirect ACL work needed. Related to Mandiant remediation findings.

2026-05-12

32

P0 - TODO

ISE Patch 10 (CVE-2026-20147 CVSS 9.9)

ISE 3.2 Patch 10. Supersedes Patch 9. 61 days on a CVSS 9.9 — schedule maintenance window. Write CR if needed.

2026-03-12

93

P0 - OVERDUE — schedule immediately

k3s NAT verification

NAT rule 170 for 10.42.0.0/16 pod network - test internet connectivity. 64 days — test this week or defer to Q3.

2026-03-09

96

P0 - BLOCKING — TRIAGE: schedule or defer

Wazuh indexer recovery

Restart pod after NAT confirmed working - SIEM visibility blocked. Blocked by k3s NAT — cannot proceed until above resolved.

2026-03-09

96

P0 - Blocked by k3s

Strongline Gateway VLAN fix

8 devices in wrong identity group (David Rukiza assigned)

2026-03-16

89

P0 - TODO

TCP Clocks deployment

ISE identity group validation, query outputs, comms with team. Active d001 data Apr 22-23.

2026-04-22

52

P0 - ACTIVE

IoT Dr. Kim — recurring

Sleep study devices (Apr 15-16), watches recurrence (Apr 22). 5 incident versions in d001. Validate iPSK enrollment.

2026-04-15

59

P0 - RECURRING

Murus Portae (WAF) — Phase 0

FMC cert expired, ACP returns zero rules. d001: zone map, architecture D2, FMC API reference, ops script.

2026-04-16

58

P0 - INVESTIGATING

Vocera EAP-TLS Supplicant Fix

~10 phones failing 802.1X, missing supplicant config. 61 days — schedule with clinical engineering team.

2026-03-12

93

P1 - TODO — schedule

ISE MnT Messaging Service

Enable "Use ISE Messaging Service for UDP syslogs delivery". 61 days — low risk, schedule with ISE Patch 10 maintenance window.

2026-03-12

93

P2 - BUNDLE with Patch 10

Professional backlog remains critical. Check Days column for priorities.

BLOCKERS — Fix Immediately

Task Details Origin Days Impact

Z Fold 7 Termux

gopass and SSH not working

2026-03-10

58

BLOCKER — Cannot access passwords on mobile

gopass v3 organization

Inconsistent structure, poor key-value usage

2026-03-20

48

Inefficient password management, no aggregation

Git history scrub — sensitive personal terms

Plaintext references to personal legal matters in committed worklogs (WRKLOG-2026-03-14, WRKLOG-2026-04-18). Forward-fixed but old commits still contain strings. Requires git filter-repo + force-push. See runbook below.

2026-04-22

15

SECURITY — sensitive terms in public git history

Runbook: Git History Scrub (d000 Personal Terms)

Problem: Two committed worklogs contained plaintext references to personal legal matters. The files have been edited (forward-fix), but git history retains the original text in prior commits.

Affected commits: Any commit touching these files:

# Identify affected commits
git log --oneline -- \
  docs/modules/ROOT/pages/2026/03/WRKLOG-2026-03-14.adoc \
  docs/modules/ROOT/pages/2026/04/WRKLOG-2026-04-18.adoc

Scrub procedure:

# 1. BEFORE: Full backup of the repo
cp -a ~/atelier/_bibliotheca/domus-captures ~/atelier/_bibliotheca/domus-captures.bak

# 2. Install git-filter-repo (if not present)
# Arch: pacman -S git-filter-repo
# pip: pip install git-filter-repo

# 3. Create expressions file for replacement
cat > /tmp/scrub-expressions.txt << 'EXPR'
regex:(?i)divorce==[REDACTED]
regex:(?i)dissolutio(?!n\.adoc\.age)==[REDACTED-LEGAL]
regex:(?i)iliana==[REDACTED-NAME]
regex:(?i)angulo-arreola==[REDACTED-NAME]
regex:legal-divorce-notes\.age==legal-notes.age
regex:1099-NEC-iliana==1099-NEC
EXPR

# 4. Verify before (dry run — count matches in history)
git log -p --all -S 'divorce' -- '*.adoc' | grep -c 'divorce' || echo "0 matches"
git log -p --all -S 'iliana' -- '*.adoc' | grep -c 'iliana' || echo "0 matches"

# 5. Run filter-repo (DESTRUCTIVE — rewrites all commit hashes)
git filter-repo --replace-text /tmp/scrub-expressions.txt --force

# 6. Verify after
git log -p --all -S 'divorce' -- '*.adoc' | grep -c 'divorce' || echo "0 matches — CLEAN"
git log -p --all -S 'iliana' -- '*.adoc' | grep -c 'iliana' || echo "0 matches — CLEAN"

# 7. Re-add remotes (filter-repo removes them)
git remote add origin git@github.com:<user>/domus-captures.git
# Add any other remotes (Gitea, etc.)

# 8. Force-push to all remotes (DESTRUCTIVE — overwrites remote history)
git remote | xargs -I{} git push {} main --force

# 9. Clean up
rm /tmp/scrub-expressions.txt
rm -rf ~/atelier/_bibliotheca/domus-captures.bak  # only after verifying

Post-scrub checklist:

  • Backup created before running

  • git filter-repo installed

  • Expressions file reviewed — no false positives (e.g., Don Quijote "Angulo el Malo" is in segunda-parte/texto/texto-011.adoc — the regex targets angulo-arreola specifically to avoid this)

  • Dry-run counts match expectations

  • Filter-repo executed

  • Post-scrub verification shows 0 matches

  • Remotes re-added

  • Force-pushed to all remotes

  • Cloudflare Pages rebuild verified

  • Local clones on other machines re-cloned or git fetch --all && git reset --hard origin/main

  • Backup removed

URGENT - Requires Immediate Action

Item Details Deadline Status Impact

Housing Search

Granada Hills area - apartments/rooms

TBD

In Progress

Quality of life, commute

2025 Tax — IRS Transcript Review

MFJ filed 2026-04-22. Pull IRS Return Transcript to verify contents. Consult attorney re: Form 8857 (Innocent Spouse Relief). Details in encrypted case file.

Before attorney meeting

In Progress

Financial — liability exposure. See encrypted D000 case file.

Rack Relocation

Physical move of server rack. CR written: CR-2026-04-18 (pending in infra-ops). Borg backup completed. VM XML dumps, switch save, shutdown/startup procedure documented.

TBD

Pending

Infrastructure downtime — all services offline during move

D000 Legal Planning

Encrypted D000 case file. Open: dissolutio-open. Close: dissolutio-close. 19 partials + assembler. PDF build for attorney handoff. Critical deadline: Jan 2029.

Before Jan 2029

Active — escalating

Life transition — see case file for details

Credit Report Review

Pull reports from all 3 bureaus via annualcreditreport.com. Verify no unknown joint accounts or debts. Credentials in gopass: v3/personal/finance/credit/annual_credit_report

TBD

In Progress

Financial discovery — FL-142 preparation

Gopass Security Audit

Rotate passwords on shared/known accounts. Add 2FA backup codes to v3/personal/recovery/. Create missing government entries (IRS, SSA, VA, DMV). Add last_login field to active entries.

TBD

Pending

Digital security — pre-filing preparation

Subscription Audit

Download 3 months bank/CC statements (Chase, NFCU, USAA). Identify all recurring charges. Cancel unnecessary. Document active subscriptions for FL-150.

TBD

Pending

Financial — expense documentation

401(k) Enrollment

Enroll in CHLA 401(k) immediately. Post-separation contributions are 100% separate property. Reduces gross income for support calculations. Max 2026: $23,500/yr.

In progress (started 5/4)

In Progress

Financial — support calculation + retirement

URGENT — Performance Review Certifications

Certification Provider Deadline Status Impact

CISSP

ISC² — Certified Information Systems Security Professional

July 12, 2026

ACTIVE — Week 2 of 10 (Project)

Required for performance review. 10-week accelerated plan.

RHCSA 9

Red Hat Certified System Administrator

Q3 2026

ACTIVE — 21-phase curriculum (Project)

After CISSP. Required for performance review.

CISSP: 41 days remaining (exam July 12). Domain 1 study in progress. Schedule exam today (06-01).

Early Morning - 5:30am

Regex Training (CRITICAL CARRYOVER)

  • Session 3 - Character classes, word boundaries

  • Practice drills from regex-mastery curriculum

  • Status: 52 days carried over (since 2026-03-16) — CRITICAL

Regex training continues to slip. This is the foundation for all CLI mastery.

Daily Notes

Triage Status

Item Status Destination

⚡ CHARGE TIME IN PEOPLESOFT — Monday critical (carryover day 2)

❌ not started

PeopleSoft → submit hours before EOD

VNC removal — uninstall/disable across affected systems

🟡 runbook created, ready to execute

daily-notes/2026-06-09/vnc-removal.adoc

rsyslog → Monad pipeline — build ingest pipeline for ASA lab logs

🟡 runbook created, architecture decided

daily-notes/2026-06-09/monad-rsyslog-pipeline.adoc

Point ISE lab, switch, FMC → rsyslog (CHLXSYSLOG01)

🟡 runbook created, source + server configs documented

daily-notes/2026-06-09/rsyslog-new-sources.adoc

Secondary NAS mount unsynced — resync failed (carryover day 2)

🟡 diagnostic runbook created

daily-notes/2026-06-09/nas-mount-resync.adoc

P1: Intune hybrid join cert auth failure (carryover day 2)

🟡 investigation file created, commands ready

decrypt-file data/d001/investigations/2026-06-08-intune-cert-auth/intune-cert-commands-2026-06-08.adoc.age

P1: Spam bypass — SCL 9 delivered to inbox (carryover day 2)

🟡 investigation file created, commands ready

spam-bypass-investigation.adoc — run Phase 1 on work machine

Install BYOD P12 cert on ZFold7 (carryover day 2)

🟡 cert issued, needs P12 bundle transfer

Phase 8: BYOD Certs

TCP Clocks batch 2 — 9 MACs added to ISE, switch NOT configured

⚠️ ISE done, switch blocking

New switch needs RADIUS/AAA config + ISE NAD entry

Fix dsec embedded quotes in vault unseal keys (carryover day 2)

❌ not started

dsec edit d000 dev/vault — remove wrapping double quotes

Rejoin AD from ISE GUI (carryover day 2)

❌ not started

Phase 6 — DOMUS_AD join point

Fix NAS SSH config (adminerosado → evanusmodestus) (carryover day 2)

❌ not started

~/.ssh/config line 245

Fix WLC SSH config (add PubkeyAuthentication no) (carryover day 2)

❌ not started

~/.ssh/config — add Host 9800-wlc-01 block

Add NTP rule to vyos-02 (carryover day 2)

❌ not started

set firewall ipv4 name MGMT_LOCAL rule 45

Add kvm-01 NAS mounts to /etc/fstab (carryover day 2)

❌ not started

10.50.1.70:/volume1/isos → /mnt/nas/isos

Generalize build-antora-page.sh for HTML output (carryover day 2)

❌ not started

scripts/build-antora-page.sh

Explore AsciiDoc CSV tables (carryover day 2)

❌ not started

codex/documentation/asciidoc.adoc or discoveries/

Printer unreachable — iPSK auth + VLAN filtering (carryover day 2)

❌ blocked

Depends on ISE rotation completing

Linux Remediation — reuse Shahab model (carryover day 2)

🟡 inventory found, summary built

d001/linux-research/ + domus-ise-linux

iTrack → Jira (Atlassian) migration (carryover day 2)

🟡 announced — team must become proficient

New project — needs codes.adoc entry, d001 scaffolding

P0/P1/P2 tracker refresh — 47 days stale (carryover day 2)

❌ not started

trackers/work/priorities/

BMS Controller Segmentation — Principia migration

✅ done

d001 open bms-controller — 12 partials, 5 diagrams, 4 PDFs

Git tree audit — remove misplaced files

✅ done

.graveyard/ created, 5 files removed from tracking

Encrypted file placement (06-08 captures)

✅ done

4 files placed in d001 investigations + siem project

TCP Clocks — one-off 40:AC:8D:00:94:10 reassigned

✅ done

Reassigned to IoT_Onboard via ERS PUT

ASA VPN Migration — Okta RADIUS → Entra SAML plan

✅ plan complete, PDF built

d001 open asa-vpn-okta — 5W’s, 5 phases, risk matrix, ISE screenshots

Abnormal — Hoxhunt SCL -1 investigation

✅ done

hoxhunt-investigation-2026-06-09.adoc

Abnormal — Policy review commands (20 sections + sclizer)

✅ done

policy-review-validation-commands.adoc

ISE Lab → rsyslog → Monad pipeline guide

✅ execution guide created

d001 open siem-qradar — 6-step with API calls

RAD Projects — scaffolded

✅ done

d001 open rad-projects

Build toolchain — Rouge + theme overhaul

✅ done

thankful_eyes, dark terminal blocks, build-adoc.sh fixed

Dashboard system — 11 SVG + 14 PNG + 12 domain diagrams

✅ done

make diagrams-dashboard + make report — 37 visual assets

YAML substrate — project-master.yaml + association engine

✅ done

37 projects in YAML, 107 triples in association engine, CLAUDE.md graph

Ad-Hoc Requests

VNC Removal — Security Hardening

VNC (Virtual Network Computing) uses the RFB protocol, which transmits session data unencrypted by default — including keyboard input, screen content, and in some configurations, authentication credentials. In a hospital environment this creates multiple risk surfaces:

  • No centralized authentication — VNC passwords are stored locally per host, outside AD/LDAP. No MFA, no group policy enforcement, no audit trail.

  • HIPAA exposure — unencrypted screen sharing of patient data over the network violates the technical safeguard requirements of 45 CFR 164.312(e)(1).

  • Unauthorized remote access — VNC servers are frequently installed by end users or vendors without change control. Ports 5900-5910 listening on production hosts represent unmanaged ingress.

  • No session logging — unlike RDP with NLA or SSH with centralized logging, VNC connections leave no audit trail in SIEM.

Authorized remote access uses approved tools (CyberArk, BeyondTrust, RDP with NLA + MFA). VNC is not on the approved software list.

Windows — Discovery and Removal
Step 1: Discover VNC Installations
Query registry for VNC entries (all common variants)
# RealVNC
Get-ItemProperty "HKLM:\SOFTWARE\RealVNC\*" -ErrorAction SilentlyContinue
Get-ItemProperty "HKLM:\SOFTWARE\WOW6432Node\RealVNC\*" -ErrorAction SilentlyContinue

# TightVNC
Get-ItemProperty "HKLM:\SOFTWARE\TightVNC" -ErrorAction SilentlyContinue
Get-ItemProperty "HKLM:\SOFTWARE\WOW6432Node\TightVNC" -ErrorAction SilentlyContinue

# UltraVNC
Get-ItemProperty "HKLM:\SOFTWARE\uvnc bvba\UltraVNC" -ErrorAction SilentlyContinue

# TigerVNC
Get-ItemProperty "HKLM:\SOFTWARE\TigerVNC" -ErrorAction SilentlyContinue
Check for VNC services
Get-Service | Where-Object { $_.DisplayName -match "vnc" -or $_.Name -match "vnc" } |
  Format-Table Name, DisplayName, Status, StartType -AutoSize
Check for VNC in installed programs
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
  "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" |
  Where-Object { $_.DisplayName -match "vnc" } |
  Select-Object DisplayName, DisplayVersion, UninstallString, InstallDate |
  Format-List
Check for VNC scheduled tasks (persistence mechanism)
Get-ScheduledTask | Where-Object { $_.TaskName -match "vnc" -or $_.TaskPath -match "vnc" } |
  Format-Table TaskName, TaskPath, State -AutoSize
Check for VNC firewall rules
Get-NetFirewallRule | Where-Object { $_.DisplayName -match "vnc" } |
  Format-Table DisplayName, Direction, Action, Enabled -AutoSize

# Also check by port — VNC rules may not mention VNC in the name
Get-NetFirewallPortFilter | Where-Object { $_.LocalPort -match "^590[0-9]$" } |
  Get-NetFirewallRule |
  Format-Table DisplayName, Direction, Action, Enabled -AutoSize
Check for listening ports in VNC range
Get-NetTCPConnection -State Listen |
  Where-Object { $_.LocalPort -ge 5900 -and $_.LocalPort -le 5910 } |
  Select-Object LocalAddress, LocalPort, OwningProcess,
    @{N='ProcessName';E={(Get-Process -Id $_.OwningProcess).ProcessName}} |
  Format-Table -AutoSize
Step 2: Stop and Disable VNC Services
Stop all VNC services immediately
Get-Service | Where-Object { $_.DisplayName -match "vnc" -or $_.Name -match "vnc" } | ForEach-Object {
    Write-Host "Stopping: $($_.DisplayName) [$($_.Name)]" -ForegroundColor Yellow
    Stop-Service -Name $_.Name -Force
    Set-Service -Name $_.Name -StartupType Disabled
    Write-Host "  Stopped and disabled." -ForegroundColor Green
}
Step 3: Uninstall VNC Variants
Uninstall via detected UninstallString (silent where possible)
# Gather uninstall entries
$vnc = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
  "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" |
  Where-Object { $_.DisplayName -match "vnc" }

foreach ($app in $vnc) {
    Write-Host "Uninstalling: $($app.DisplayName)" -ForegroundColor Yellow
    Write-Host "  UninstallString: $($app.UninstallString)" -ForegroundColor Cyan

    # Attempt silent uninstall — adjust flags per vendor
    if ($app.UninstallString -match "msiexec") {
        # MSI-based: add /quiet /norestart
        $msiArgs = ($app.UninstallString -replace "msiexec.exe\s*", "") + " /quiet /norestart"
        Start-Process "msiexec.exe" -ArgumentList $msiArgs -Wait
    } else {
        # EXE-based: try /S (NSIS) or /SILENT (Inno Setup)
        $exe = $app.UninstallString -replace '"', ''
        Start-Process $exe -ArgumentList "/S" -Wait -ErrorAction SilentlyContinue
    }
}
Manual uninstall commands for specific variants (if auto-detect misses)
# RealVNC Server
& "C:\Program Files\RealVNC\VNC Server\vnclicense.exe" -deactivate 2>$null
& "C:\Program Files\RealVNC\VNC Server\vncserver.exe" -unregister 2>$null

# TightVNC — typical MSI GUID (verify with registry query above)
# msiexec /x {YOUR-GUID-HERE} /quiet /norestart

# UltraVNC
& "C:\Program Files\uvnc bvba\UltraVNC\unins000.exe" /SILENT 2>$null
Step 4: Remove Firewall Rules
Remove VNC firewall rules
# Remove rules named with VNC
Get-NetFirewallRule | Where-Object { $_.DisplayName -match "vnc" } | ForEach-Object {
    Write-Host "Removing firewall rule: $($_.DisplayName)" -ForegroundColor Yellow
    Remove-NetFirewallRule -Name $_.Name
}

# Remove rules allowing VNC ports regardless of name
Get-NetFirewallPortFilter | Where-Object { $_.LocalPort -match "^590[0-9]$" } |
  Get-NetFirewallRule | ForEach-Object {
    Write-Host "Removing port-based rule: $($_.DisplayName) [port filter match]" -ForegroundColor Yellow
    Remove-NetFirewallRule -Name $_.Name
}
Step 5: Clean Up Residual Files
Remove leftover directories
$vnc_paths = @(
    "$env:ProgramFiles\RealVNC",
    "${env:ProgramFiles(x86)}\RealVNC",
    "$env:ProgramFiles\TightVNC",
    "${env:ProgramFiles(x86)}\TightVNC",
    "$env:ProgramFiles\uvnc bvba",
    "${env:ProgramFiles(x86)}\uvnc bvba",
    "$env:ProgramFiles\TigerVNC",
    "${env:ProgramFiles(x86)}\TigerVNC"
)

foreach ($path in $vnc_paths) {
    if (Test-Path $path) {
        Write-Host "Removing: $path" -ForegroundColor Yellow
        Remove-Item -Path $path -Recurse -Force
    }
}
GPO Enforcement (Preventive)

Deploy a Group Policy to block reinstallation:

  • Software Restriction Policy or AppLocker rule blocking execution of vncserver.exe, tvnserver.exe, winvnc.exe, vncviewer.exe

  • Windows Firewall GPO denying inbound TCP 5900-5910 at the domain level

  • SCCM/Intune compliance rule flagging VNC as non-compliant software

These require AD/GPO admin access — coordinate with the Windows team and document as a CR per STD-005.

Linux — Discovery and Removal
Step 1: Discover VNC Installations
Check for VNC services (systemd)
# Active VNC services
systemctl list-units --type=service --all | grep -i vnc

# Enabled VNC services (survive reboot)
systemctl list-unit-files --type=service | grep -i vnc
Check installed packages — Debian/Ubuntu
dpkg -l | awk '/vnc|tigervnc|tightvnc|x11vnc|vino/{print $1, $2, $3}'
Check installed packages — RHEL/CentOS/Rocky
rpm -qa | grep -iE 'vnc|tigervnc|tightvnc|x11vnc|vino'
Check for listening ports in VNC range
ss -tlnp | awk '$4 ~ /:590[0-9]/ {print}'
# Example output:
# LISTEN  0  5  0.0.0.0:5901  0.0.0.0:*  users:(("Xvnc",pid=1234,fd=7))
Check for VNC processes
ps aux | awk '/[Vv]nc|[Xx]11vnc|[Vv]ino/{print $1, $2, $11}'
Check for VNC configuration files
find /etc /home -name "*.vnc" -o -name "xstartup" -o -name ".vnc" -type d 2>/dev/null
Step 2: Stop and Remove
Stop VNC services
# Stop all VNC-related services
for svc in $(systemctl list-units --type=service --all --no-legend | awk '/vnc/{print $1}'); do
    echo "Stopping: ${svc}"
    sudo systemctl stop "${svc}"
    sudo systemctl disable "${svc}"
    echo "  Stopped and disabled."
done
Remove VNC packages — Debian/Ubuntu
# Verify what will be removed first
apt list --installed 2>/dev/null | grep -iE 'vnc|tigervnc|tightvnc|x11vnc|vino'

# Remove (purge removes config files too)
sudo apt purge -y tigervnc-standalone-server tigervnc-common \
    tightvncserver x11vnc vino 2>/dev/null
sudo apt autoremove -y
Remove VNC packages — RHEL/CentOS/Rocky
# Verify first
rpm -qa | grep -iE 'vnc|tigervnc|tightvnc|x11vnc|vino'

# Remove
sudo dnf remove -y tigervnc-server tigervnc x11vnc 2>/dev/null
Step 3: Firewall Cleanup
UFW (Debian/Ubuntu)
# Check for VNC rules
sudo ufw status numbered | grep -i "590[0-9]"

# Delete by rule number (adjust number from output above)
# sudo ufw delete <rule-number>

# Or deny the range explicitly
sudo ufw deny 5900:5910/tcp comment "Block VNC — unauthorized remote access"
sudo ufw reload
firewalld (RHEL/CentOS/Rocky)
# Check current rules
sudo firewall-cmd --list-all | grep -E "590[0-9]|vnc"

# Remove VNC service if added
sudo firewall-cmd --permanent --remove-service=vnc-server 2>/dev/null

# Block VNC ports explicitly
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="5900-5910" protocol="tcp" reject'
sudo firewall-cmd --reload
Step 4: Clean Up User VNC Directories
Remove per-user VNC configurations
# Find and list .vnc directories across all home dirs
find /home -maxdepth 2 -name ".vnc" -type d 2>/dev/null

# Remove them (after confirming the list above)
# find /home -maxdepth 2 -name ".vnc" -type d -exec rm -rf {} +
Network Infrastructure — Management Access Audit

VNC is not a standard management protocol for network gear, but unauthorized access methods should be verified during the sweep.

Cisco ASA
Check running config for VNC or unexpected management listeners
show run | include vnc
show run | include http server
show run | include ssh
show run management-access
Cisco IOS/IOS-XE Switches
Verify management access is restricted to approved protocols
show run | section line vty
show run | include ip http
show ip ssh
show run | include access-class
Firepower Management Center (FMC)
Audit management access settings
# FMC management is web-based (HTTPS 443)
# Verify no additional listeners on unexpected ports
show network          # FMC CLI
show access-list      # Check for VNC port permits on managed firewalls

The primary concern on network gear is ensuring ACLs do not permit VNC ports (5900-5910) through the infrastructure. Audit firewall rules for these ports:

Check ACLs for VNC port permits
# ASA
show access-list | include 5900
show access-list | include 5901

# IOS-XE
show ip access-lists | include 5900
Post-Removal Verification
Port Scan Validation
Scan target host for VNC ports (run from your workstation)
# Single host
# Replace <target-ip> with the remediated host
# Example: nmap -p 5900-5910 10.50.4.100
nmap -sT -p 5900-5910 <target-ip>

# Subnet sweep — find any remaining VNC listeners
# Example: nmap -p 5900-5910 10.50.4.0/24
nmap -sT -p 5900-5910 --open <subnet-cidr>

Expected result: all ports show closed or filtered, never open.

Service Validation
Windows — confirm no VNC services remain
# Services
Get-Service | Where-Object { $_.DisplayName -match "vnc" }
# Expected: no output

# Listening ports
Get-NetTCPConnection -State Listen |
  Where-Object { $_.LocalPort -ge 5900 -and $_.LocalPort -le 5910 }
# Expected: no output

# Registry
Get-ItemProperty "HKLM:\SOFTWARE\*VNC*", "HKLM:\SOFTWARE\WOW6432Node\*VNC*" -ErrorAction SilentlyContinue
# Expected: no output

# Installed programs
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" |
  Where-Object { $_.DisplayName -match "vnc" }
# Expected: no output
Linux — confirm no VNC services remain
# Services
systemctl list-units --type=service --all | grep -i vnc
# Expected: no output

# Listening ports
ss -tlnp | awk '$4 ~ /:590[0-9]/'
# Expected: no output

# Packages
dpkg -l 2>/dev/null | grep -i vnc    # Debian
rpm -qa 2>/dev/null | grep -i vnc    # RHEL
# Expected: no output (or status 'rc' = removed, config remaining)

# Processes
pgrep -ai vnc
# Expected: no output
Validation Checklist
Step Check Result

1

No VNC services running (Get-Service / systemctl)

[ ] Pass

2

No VNC ports listening (netstat/ss/Get-NetTCPConnection on 5900-5910)

[ ] Pass

3

No VNC packages installed (dpkg -l / rpm -qa / registry)

[ ] Pass

4

No VNC firewall allow rules remain

[ ] Pass

5

VNC ports blocked by host firewall (UFW/firewalld/Windows Firewall)

[ ] Pass

6

Network ACLs deny 5900-5910 inbound

[ ] Pass

7

Nmap scan confirms no open VNC ports on remediated hosts

[ ] Pass

8

GPO/Intune compliance rule deployed (Windows — preventive)

[ ] Pass

Evidence Collection

Document remediation per host for the security team:

# Capture evidence to a timestamped file per host
# Example: HOSTNAME=ws-radiology-04
echo "=== VNC Removal Evidence — $(hostname) ===" > "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
echo "Date: $(date)" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
echo "" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"

echo "--- Services ---" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
systemctl list-units --type=service --all 2>/dev/null | grep -i vnc >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
echo "(none found)" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"

echo "--- Listening Ports ---" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
ss -tlnp | awk 'NR==1 || $4 ~ /:590[0-9]/' >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"

echo "--- Packages ---" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
dpkg -l 2>/dev/null | grep -i vnc >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
rpm -qa 2>/dev/null | grep -i vnc >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"

echo "--- Nmap ---" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"
echo "Run from scanning host: nmap -sT -p 5900-5910 $(hostname -I | awk '{print $1}')" >> "/tmp/vnc-removal-$(hostname)-$(date +%F).txt"

rsyslog — Adding ISE, Switch, and FMC Sources

Three new log sources for CHLXSYSLOG01. ASA is already flowing. Same architecture: device sends syslog to 10.248.13.254:514, rsyslog writes per-source log files, Monad ingests downstream.

Part 1: Source-Side Configuration
1A — Cisco ISE (Home Lab)

ISE sends syslog natively. Two steps: define the remote target, then assign it to logging categories.

Step 1 — Add Remote Logging Target (GUI)
Administration → System → Logging → Remote Logging Targets
  → Add

  Name:           CHLXSYSLOG01
  IP Address:     10.248.13.254
  Port:           514
  Facility:       LOCAL6
  Max Length:     8192
  Include Alarms: Yes (optional — adds ISE health alarms to syslog)
Step 2 — Assign Target to Logging Categories (GUI)
Administration → System → Logging → Logging Categories

  Category                          → Enable → Target: CHLXSYSLOG01
  ─────────────────────────────────────────────────────────────────
  Passed Authentications            → ✓
  Failed Attempts                   → ✓
  RADIUS Diagnostics                → ✓
  Authentication Flow Diagnostics   → ✓
  Administrator Audits              → ✓ (optional — tracks admin logins)
  System Diagnostics                → ✓ (optional — ISE health)
  Posture and Client Provisioning   → ✓ (optional)
Step 2 alt — CLI alternative (ISE application shell)
# SSH to ISE, enter application shell
# ISE does not expose syslog target config via CLI — GUI only.
# CLI can verify after configuration:
show logging application
show logging system
Key ISE syslog message IDs to expect
CISE_Passed_Authentications     — Successful 802.1X/MAB/RADIUS auths
CISE_Failed_Attempts            — Auth failures (wrong cert, expired, policy deny)
CISE_RADIUS_Diagnostics         — RADIUS protocol-level events
CISE_RADIUS_Accounting          — Accounting start/stop/interim
CISE_Administrative_and_Operational_Audit — Admin GUI/CLI actions
CISE_Profiler                   — Endpoint profiling events
CISE_System_Diagnostics         — ISE node health, replication
CISE_Threat_Centric_NAC         — TC-NAC verdicts (if enabled)
1B — Cisco Switch (IOS / IOS-XE)
Configure syslog on the switch
conf t
logging host 10.248.13.254
logging trap informational
logging facility local6
logging source-interface Loopback0    (1)
logging on
end
1 Use the management or loopback interface so the source IP is deterministic. Replace Loopback0 with the actual management interface (Vlan10, GigabitEthernet0/0, etc.).
Verify — on the switch
show logging
show logging | include Logging to
Expected output
Logging to 10.248.13.254 (udp port 514, audit disabled,
    link up),
    27 message lines logged,
    0 message lines rate-limited,
    0 message lines dropped-by-MD,
    xml disabled, sequence number disabled
    filtering disabled
Logging Source-Interface: Loopback0
Key IOS syslog message IDs to expect
%SEC-6-IPACCESSLOGP     — ACL permit/deny hits
%LINK-3-UPDOWN          — Interface state changes
%LINEPROTO-5-UPDOWN     — Line protocol state changes
%SYS-5-CONFIG_I         — Config change from terminal
%AUTHMGR-5-*            — 802.1X auth manager events
%DOT1X-5-*              — 802.1X protocol events
%MAB-5-*                — MAB authentication events
%EPM-6-POLICY_APP*      — Endpoint policy application (dACL, VLAN)
%RADIUS-4-RADIUS_DEAD   — RADIUS server unreachable
1C — Cisco FMC (Firepower Management Center)

FMC sends its own management/audit logs. FTD sensors send connection/intrusion events separately.

Step 1 — FMC Syslog Server (GUI)
System → Configuration → Logging → Syslog Server
  → Add Syslog Server

  IP Address:    10.248.13.254
  Port:          514
  Protocol:      UDP
  Facility:      LOCAL5

  Enable logging for:
    ✓ Health Events
    ✓ Audit Logs
    ✓ Intrusion Events (if applicable)
    ✓ Connection Events (high volume — enable selectively)
Step 2 — FTD Managed Device (if sending directly from sensor)
! On FMC: Devices → Platform Settings → <policy> → Syslog
! Or via FlexConfig:
platform logging host inside 10.248.13.254 udp/514
platform logging trap informational
platform logging facility local5
FTD platform logging requires a FlexConfig policy deployed from FMC. Direct SSH to FTD does not persist syslog config.
Key FMC/FTD syslog fields to expect
FMC audit logs:
  "User '<user>' ... from <ip>"             — Admin login/logout
  "Policy '<name>' ... deployed"            — Policy deployment
  "Task '<name>' ... completed/failed"      — Scheduled task results

FTD connection events (if enabled):
  %FTD-6-430003   — Connection permitted
  %FTD-4-430002   — Connection denied
  %FTD-1-430001   — Intrusion event

FTD intrusion events:
  GID:SID format (e.g., 1:40000)           — Snort rule triggers
  Classification, Priority, Source/Dest     — Full 5-tuple

Health events:
  "Health Alert: <module> ... <status>"     — Disk, CPU, interface health
Part 2: rsyslog Server-Side Configuration (CHLXSYSLOG01)

Three config files under /etc/rsyslog.d/. Numbering convention: 10-asa.conf already exists from 2026-06-08, so these start at 20-.

Pre-check — current state before changes
Verify existing configs and log directories
# What configs exist now?
ls -la /etc/rsyslog.d/

# Is the ASA config from yesterday still active?
sudo awk '/^[^#]/ && NF' /etc/rsyslog.d/10-asa.conf

# Current disk usage — baseline before adding sources
df -h /var | awk 'NR==1 || /var/'
du -sh /var/log/asa/ 2>/dev/null || echo "No ASA log dir yet"
2A — ISE Config
Create /etc/rsyslog.d/20-ise.conf
sudo tee /etc/rsyslog.d/20-ise.conf << 'RSYSLOG'
# ISE syslog — home lab ISE instance
# Source IP: <ISE_IP> (replace with actual ISE management IP)
# Facility: local6 (set on ISE remote logging target)
# Created: 2026-06-09

template(name="IseFile" type="string"
    string="/var/log/ise/%HOSTNAME%/%$YEAR%-%$MONTH%-%$DAY%.log")

if $fromhost-ip == '<ISE_IP>' then {
    action(type="omfile" dynaFile="IseFile"
           FileCreateMode="0640"
           DirCreateMode="0750"
           CreateDirs="on")
    stop
}
RSYSLOG
Replace <ISE_IP> with the actual management IP of the ISE node. This is the IP that rsyslog will see as the source. Confirm with ip -br addr on ISE or check the ISE GUI under Administration → System → Deployment → Node.
Verify the file was written correctly
sudo awk '/^[^#]/ && NF' /etc/rsyslog.d/20-ise.conf
2B — Switch Config
Create /etc/rsyslog.d/20-switch.conf
sudo tee /etc/rsyslog.d/20-switch.conf << 'RSYSLOG'
# Network switch syslog — IOS/IOS-XE
# Source IP: <SWITCH_IP> (replace with switch management/source-interface IP)
# Facility: local6 (set on switch: logging facility local6)
# Created: 2026-06-09

template(name="SwitchFile" type="string"
    string="/var/log/switch/%HOSTNAME%/%$YEAR%-%$MONTH%-%$DAY%.log")

if $fromhost-ip == '<SWITCH_IP>' then {
    action(type="omfile" dynaFile="SwitchFile"
           FileCreateMode="0640"
           DirCreateMode="0750"
           CreateDirs="on")
    stop
}
RSYSLOG
Replace <SWITCH_IP> with the IP of the switch’s logging source-interface. If the switch has no source-interface configured, rsyslog sees whatever egress IP the switch routes from.
Verify
sudo awk '/^[^#]/ && NF' /etc/rsyslog.d/20-switch.conf
2C — FMC Config
Create /etc/rsyslog.d/20-fmc.conf
sudo tee /etc/rsyslog.d/20-fmc.conf << 'RSYSLOG'
# FMC syslog — Cisco Firepower Management Center
# Source IP: <FMC_IP> (replace with FMC management IP)
# Facility: local5 (set on FMC syslog server config)
# Created: 2026-06-09

template(name="FmcFile" type="string"
    string="/var/log/fmc/%HOSTNAME%/%$YEAR%-%$MONTH%-%$DAY%.log")

if $fromhost-ip == '<FMC_IP>' then {
    action(type="omfile" dynaFile="FmcFile"
           FileCreateMode="0640"
           DirCreateMode="0750"
           CreateDirs="on")
    stop
}
RSYSLOG
Verify
sudo awk '/^[^#]/ && NF' /etc/rsyslog.d/20-fmc.conf
Config load order

The full /etc/rsyslog.d/ should now look like:

/etc/rsyslog.d/
├── 10-asa.conf         ← existing (2026-06-08)
├── 20-fmc.conf         ← new
├── 20-ise.conf         ← new
├── 20-switch.conf      ← new
└── (any other existing configs)

Order matters: rsyslog processes if/stop rules top-down. The stop directive ensures a message matching one source does not also land in the default syslog. Numbered prefixes control evaluation order.

Part 3: Validation
Step 1 — Syntax check before restart
Validate rsyslog configuration (non-destructive)
sudo /usr/sbin/rsyslogd -N1
Expected output
rsyslogd: version 8.2504.0, config validation run (level 1)...
rsyslogd: End of config validation run. Bye.

If errors appear, fix the offending config file. Common mistakes: missing closing brace, unquoted IP, typo in template name.

Step 2 — Restart rsyslog
Restart and verify
# Before — confirm current PID and uptime
systemctl show rsyslog --property=MainPID,ActiveEnterTimestamp

# Change
sudo systemctl restart rsyslog

# After — confirm new PID and clean status
systemctl show rsyslog --property=MainPID,ActiveEnterTimestamp
systemctl is-active rsyslog && echo "rsyslog running" || echo "FAILED"
sudo journalctl -u rsyslog --since "30 seconds ago" --no-pager
Step 3 — Verify packets arriving per source
tcpdump — one per source (run these as you enable each device)
# ISE — filter by ISE source IP
sudo tcpdump -i any src host <ISE_IP> and port 514 -nn -c 5

# Switch — filter by switch source IP
sudo tcpdump -i any src host <SWITCH_IP> and port 514 -nn -c 5

# FMC — filter by FMC source IP
sudo tcpdump -i any src host <FMC_IP> and port 514 -nn -c 5
Step 4 — Verify logs landing on disk
Check log directories were created and files are populating
# ISE
ls -la /var/log/ise/ 2>/dev/null && tail -5 /var/log/ise/*/$(date +%Y-%m-%d).log 2>/dev/null || echo "No ISE logs yet"

# Switch
ls -la /var/log/switch/ 2>/dev/null && tail -5 /var/log/switch/*/$(date +%Y-%m-%d).log 2>/dev/null || echo "No switch logs yet"

# FMC
ls -la /var/log/fmc/ 2>/dev/null && tail -5 /var/log/fmc/*/$(date +%Y-%m-%d).log 2>/dev/null || echo "No FMC logs yet"
Live tail — all three in parallel (tmux panes)
# Pane 1: ISE
sudo tail -f /var/log/ise/*/$(date +%Y-%m-%d).log

# Pane 2: Switch
sudo tail -f /var/log/switch/*/$(date +%Y-%m-%d).log

# Pane 3: FMC
sudo tail -f /var/log/fmc/*/$(date +%Y-%m-%d).log
Step 5 — Trigger test events
ISE — generate a test authentication
# From a wired endpoint, disconnect/reconnect the cable.
# Or from ISE CLI:
show logging application | tail -20
# Any RADIUS auth attempt generates CISE_Passed_Authentications or CISE_Failed_Attempts
Switch — generate a test log
! On the switch — any config change generates %SYS-5-CONFIG_I
conf t
hostname <current-hostname>
end
! Or shut/no shut a test interface
FMC — generate a test log
FMC GUI: System → Health → Health Monitor → Run All Modules
This generates health event syslogs immediately.
Or: make any policy change → deploy → generates audit log.
Validation Checklist
Source Verification Command Expected Result

ISE — target configured

ISE GUI: Administration → System → Logging → Remote Logging Targets

CHLXSYSLOG01 listed, IP 10.248.13.254, port 514

ISE — categories assigned

ISE GUI: Administration → System → Logging → Logging Categories

Passed Auths + Failed Attempts → CHLXSYSLOG01

Switch — logging host set

show logging | include Logging to

Logging to 10.248.13.254

Switch — facility correct

show logging | include Facility

local6

FMC — syslog server added

FMC GUI: System → Configuration → Logging

10.248.13.254 UDP 514 listed

rsyslog — config valid

sudo /usr/sbin/rsyslogd -N1

End of config validation run. Bye.

rsyslog — service running

systemctl is-active rsyslog

active

rsyslog — listening

ss -ulnp | grep 514

udp *:514 (unchanged from before)

ISE — packets arriving

sudo tcpdump -i any src host <ISE_IP> and port 514 -c 3

Packets captured from ISE IP

Switch — packets arriving

sudo tcpdump -i any src host <SWITCH_IP> and port 514 -c 3

Packets captured from switch IP

FMC — packets arriving

sudo tcpdump -i any src host <FMC_IP> and port 514 -c 3

Packets captured from FMC IP

ISE — logs on disk

ls /var/log/ise/

Directory with hostname subdirectory, today’s date file

Switch — logs on disk

ls /var/log/switch/

Directory with hostname subdirectory, today’s date file

FMC — logs on disk

ls /var/log/fmc/

Directory with hostname subdirectory, today’s date file

No default syslog bleed

tail -50 /var/log/syslog | grep -c '<ISE_IP>|<SWITCH_IP>|<FMC_IP>'

0stop directive prevents duplicate writes

Post-Validation: Logrotate

The dynamic file paths (/var/log/ise/, /var/log/switch/, /var/log/fmc/) are not covered by the default /etc/logrotate.d/rsyslog. Add rotation or these directories will grow unbounded.

Check existing logrotate config
cat /etc/logrotate.d/rsyslog
Create /etc/logrotate.d/rsyslog-sources (if not already handling custom paths)
sudo tee /etc/logrotate.d/rsyslog-sources << 'LOGROTATE'
/var/log/asa/*/*.log
/var/log/ise/*/*.log
/var/log/switch/*/*.log
/var/log/fmc/*/*.log
{
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0640 syslog adm
    sharedscripts
    postrotate
        /usr/lib/rsyslog/rsyslog-rotate
    endscript
}
LOGROTATE
Verify logrotate config syntax
sudo logrotate -d /etc/logrotate.d/rsyslog-sources

Monad Pipeline — rsyslog Forwarding Configuration

Architecture

The rsyslog server acts as a centralized buffer between log sources and Monad. This is deliberately not a pass-through — the disk-assisted queue on rsyslog absorbs bursts, survives Monad outages, and decouples source availability from pipeline health.

ASA Lab ──────┐
ISE Lab ──────┤
Switch ───────┤──> rsyslog (CHLXSYSLOG01) ──> Monad Pipeline ──> Sentinel
FMC ──────────┤       10.248.13.254            (syslog input)     (output)
Win Hosts ────┘            │
                      disk queue
                    (500MB buffer)
Part 1: rsyslog Forwarding to Monad (on CHLXSYSLOG01)

The omfwd module ships with rsyslog — no extra packages. It forwards syslog over TCP (optionally TLS) to Monad’s syslog input endpoint. The critical advantage is the disk-assisted queue: if Monad is unreachable, rsyslog buffers to disk and retries automatically. Direct source-to-Monad has no such buffer — when Monad drops, logs are lost.

Create the forwarding config
sudo tee /etc/rsyslog.d/90-monad-forward.conf << 'RSYSLOG'
# Forward all collected logs to Monad syslog input
# Disk-assisted queue provides backpressure and survives outages
#
# queue.type="LinkedList"         — in-memory linked list (fast, low overhead)
# queue.filename="monad-fwd"     — enables disk assistance; files land in WorkDirectory
# queue.maxDiskSpace="500m"      — cap disk usage at 500MB before dropping
# queue.saveOnShutdown="on"      — flush in-memory queue to disk on rsyslog stop/restart
# action.resumeRetryCount="-1"   — retry forever (never give up on Monad)
# action.resumeInterval="30"     — wait 30s between retry attempts
#
# template: RSYSLOG_SyslogProtocol23Format = RFC 5424 structured syslog
#   Monad's syslog input expects RFC 5424. If it chokes, try RSYSLOG_TraditionalForwardFormat (BSD).

action(type="omfwd"
       target="<MONAD_SYSLOG_ENDPOINT>"
       port="<PORT>"
       protocol="tcp"
       template="RSYSLOG_SyslogProtocol23Format"
       queue.type="LinkedList"
       queue.filename="monad-fwd"
       queue.maxDiskSpace="500m"
       queue.saveOnShutdown="on"
       action.resumeRetryCount="-1"
       action.resumeInterval="30")
RSYSLOG

Replace <MONAD_SYSLOG_ENDPOINT> and <PORT> with the values from the Monad syslog input configuration (Part 2, Step 2). The endpoint and port are assigned when you create the syslog input in Monad — create the input first, then come back here.

Queue Parameters Explained
Parameter Purpose

queue.type="LinkedList"

In-memory linked list. Lower overhead than FixedArray for bursty traffic. When the in-memory portion fills, it spills to disk via the filename.

queue.filename="monad-fwd"

Enables disk assistance. Without this, the queue is memory-only and data is lost on restart. Files are created in rsyslog’s WorkDirectory (typically /var/spool/rsyslog/).

queue.maxDiskSpace="500m"

Hard cap on disk usage. At ~200 bytes per syslog message, 500MB holds roughly 2.5 million messages. Tune based on expected outage duration and message rate.

queue.saveOnShutdown="on"

On systemctl stop rsyslog or systemctl restart rsyslog, the in-memory queue is flushed to disk. Without this, an rsyslog restart during a Monad outage loses the in-memory buffer.

action.resumeRetryCount="-1"

Retry forever. The value -1 means infinite retries. Without this, rsyslog gives up after 30 attempts (default) and discards the action.

action.resumeInterval="30"

Seconds between retry attempts when the action is suspended (Monad unreachable). 30 seconds is conservative — not aggressive enough to hammer, frequent enough to recover quickly.

This is the backpressure advantage. Direct source-to-Monad forwarding (the rolled-back ASA config) has none of this — when the destination is down, messages hit the floor.

Option B: omhttp — HTTP POST Forwarding (Alternative)

If Monad exposes a REST/HTTP input endpoint (not just syslog), omhttp can POST log batches as JSON. This requires the rsyslog-omhttp package.

Check if omhttp is available
dpkg -l | awk '/rsyslog.*http/'
# If not installed:
# sudo apt install rsyslog-omhttp
omhttp config (only if Monad has an HTTP input)
sudo tee /etc/rsyslog.d/90-monad-forward-http.conf << 'RSYSLOG'
module(load="omhttp")

template(name="MonadJSON" type="list") {
    constant(value="{\"message\":\"")
    property(name="msg" format="jsonf")
    constant(value="\",\"host\":\"")
    property(name="hostname")
    constant(value="\",\"facility\":\"")
    property(name="syslogfacility-text")
    constant(value="\",\"severity\":\"")
    property(name="syslogseverity-text")
    constant(value="\",\"timestamp\":\"")
    property(name="timereported" dateformat="rfc3339")
    constant(value="\"}")
}

action(type="omhttp"
       server="<MONAD_HTTP_ENDPOINT>"
       serverport="443"
       restpath="<MONAD_HTTP_PATH>"
       usehttps="on"
       httpheaderkey="Authorization"
       httpheadervalue="ApiKey <MONAD_API_KEY>"
       template="MonadJSON"
       batch="on"
       batch.maxsize="100"
       queue.type="LinkedList"
       queue.filename="monad-http-fwd"
       queue.maxDiskSpace="500m"
       queue.saveOnShutdown="on"
       action.resumeRetryCount="-1"
       action.resumeInterval="30")
RSYSLOG

Option A (omfwd/syslog) is strongly preferred. The syslog input on Monad handles parsing, TLS cert routing, and native syslog semantics. Option B requires additional package installation, JSON template maintenance, and embeds the API key in rsyslog config. Use Option B only if Monad’s syslog endpoint is unavailable or if you need structured JSON ingestion.

Apply and Verify rsyslog Config
Validate config syntax before restarting
sudo rsyslogd -N1 2>&1 | tail -5
Restart rsyslog
sudo systemctl restart rsyslog
sudo systemctl status rsyslog --no-pager
Verify the queue spool directory exists
ls -la /var/spool/rsyslog/
# Should see monad-fwd.* files appear once messages start queuing
Part 2: Monad Pipeline Creation (API Calls from Workstation)

All commands run from your workstation. Requires MONAD_API_KEY and MONAD_API_URL environment variables, plus MONAD_ORG_ID.

Verify environment
printf "API URL: %s\nOrg ID:  %s\nKey set: %s\n" \
  "${MONAD_API_URL:-NOT SET}" \
  "${MONAD_ORG_ID:-NOT SET}" \
  "$([ -n "${MONAD_API_KEY:-}" ] && echo yes || echo NO)"
Step 1: List Existing Pipelines
List all pipelines
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines" | \
  jq '.pipelines[] | {name, id, enabled}'
Step 2: Create Syslog Input

Create a syslog input that will receive forwarded logs from rsyslog (CHLXSYSLOG01).

Search the input catalog for syslog type
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/inputs" | \
  jq '.[] | select(.name | test("syslog"; "i")) | {type_id, name, description}'
Create syslog input
curl -s -X POST \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/inputs" \
  -d '{
    "name": "CHLXSYSLOG01-Forward",
    "description": "Syslog forwarded from centralized rsyslog collector (10.248.13.254)",
    "type": "monad-syslog",
    "config": {
      "settings": {}
    }
  }' | jq '.'

Capture the id from the response — this is the input component ID needed for the pipeline node. Also note the assigned host and port in the response config — these are the values for the rsyslog omfwd target and port in Part 1.

Save the input ID
SYSLOG_INPUT_ID="<id-from-response>"
Step 3: Create the Pipeline
Create pipeline
curl -s -X POST \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines" \
  -d '{
    "name": "CHLA-rsyslog-to-Sentinel",
    "description": "Centralized rsyslog (CHLXSYSLOG01) forwarding to Microsoft Sentinel",
    "enabled": false
  }' | jq '.'
Save the pipeline ID
PIPELINE_ID="<id-from-response>"
Step 4: Add Syslog Input Node to Pipeline
Add input node
INPUT_NODE_ID=$(cat /proc/sys/kernel/random/uuid)

curl -s -X PATCH \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines/${PIPELINE_ID}" \
  -d "$(jq -n \
    --arg node_id "$INPUT_NODE_ID" \
    --arg input_id "$SYSLOG_INPUT_ID" \
    '{
      nodes: [{
        id: $node_id,
        slug: ("input-" + ($node_id | split("-")[0])),
        component_id: $input_id,
        component_type: "input",
        enabled: true
      }]
    }')" | jq '.'
Step 5: Create or Reference Sentinel Output
Search for existing Sentinel outputs
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/outputs" | \
  jq '.outputs[] | select(.name | test("sentinel"; "i")) | {name, id, type}'

If a Sentinel output already exists, reuse its ID. Otherwise, create one:

Search the output catalog for Sentinel type
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/outputs" | \
  jq '.[] | select(.name | test("sentinel"; "i")) | {type_id, name}'
Create Sentinel output (if none exists)
curl -s -X POST \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/outputs" \
  -d '{
    "name": "CHLA-Sentinel",
    "description": "Microsoft Sentinel workspace for CHLA SIEM",
    "type": "microsoft-sentinel",
    "config": {
      "settings": {
        "tenant_id": "<AZURE_TENANT_ID>",
        "client_id": "<AZURE_CLIENT_ID>",
        "client_secret": "<AZURE_CLIENT_SECRET>",
        "workspace_id": "<LOG_ANALYTICS_WORKSPACE_ID>",
        "table_name": "Syslog"
      }
    }
  }' | jq '.'
Save the output ID
SENTINEL_OUTPUT_ID="<id-from-response-or-existing>"
Step 6: Add Output Node and Create Edge
Add output node and edge connecting input to output
OUTPUT_NODE_ID=$(cat /proc/sys/kernel/random/uuid)

curl -s -X PATCH \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines/${PIPELINE_ID}" \
  -d "$(jq -n \
    --arg input_node "$INPUT_NODE_ID" \
    --arg output_node "$OUTPUT_NODE_ID" \
    --arg output_id "$SENTINEL_OUTPUT_ID" \
    '{
      nodes: [{
        id: $output_node,
        slug: ("output-" + ($output_node | split("-")[0])),
        component_id: $output_id,
        component_type: "output",
        enabled: true
      }],
      edges: [{
        name: "always",
        from_node_instance_id: $input_node,
        to_node_instance_id: $output_node,
        conditions: { operator: "always" }
      }]
    }')" | jq '.'

The edge condition "operator": "always" forwards all messages unconditionally. To filter, replace with a rule-based condition:

Example: filter edge — only forward ASA messages
{
  "conditions": {
    "operator": "and",
    "rules": [{
      "field": "message",
      "operator": "contains",
      "value": "%ASA-"
    }]
  }
}

Available edge operators: always, never, and, or, nor, xor.
Available rule operators: contains, ends_with, equals, exists, matches, starts_with.

Step 7: Activate the Pipeline
Enable the pipeline
curl -s -X PATCH \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/pipelines/${PIPELINE_ID}" \
  -d '{"enabled": true}' | jq '{id, name, enabled}'
Step 8: Verify Pipeline Status
Get full pipeline details (v2 — includes nodes and edges)
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines/${PIPELINE_ID}" | \
  jq '{
    name, enabled,
    nodes: [.nodes[] | {slug, component_type, enabled}],
    edges: [.edges[] | {name, from: .from_node_instance_id[:8], to: .to_node_instance_id[:8]}]
  }'
Check pipeline logs for errors
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/logs/pipelines/${PIPELINE_ID}" | \
  jq '.logs[:10] | .[] | {timestamp, level, message}'
Part 3: Validation
Monad Pipeline Metrics
Check pipeline processing metrics
curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/logs/pipelines/${PIPELINE_ID}" | \
  jq '{
    total_logs: (.logs | length),
    errors: [.logs[] | select(.level == "error")] | length,
    latest: .logs[0].timestamp
  }'
rsyslog Queue Status (on CHLXSYSLOG01)
Check queue depth and disk usage
# Queue spool files — if these exist, messages are buffered (Monad was/is unreachable)
ls -lh /var/spool/rsyslog/monad-fwd.* 2>/dev/null || echo "No queue files (good — messages flowing)"

# rsyslog internal stats (if impstats is enabled)
grep 'monad-fwd' /var/log/rsyslog-stats.log 2>/dev/null | tail -5

# Quick health: is rsyslog forwarding or queuing?
sudo journalctl -u rsyslog --since "10 min ago" --no-pager | \
  awk '/monad|queue|suspend|resume/'
Check rsyslog action stats
# Enable impstats if not already (add to /etc/rsyslog.conf)
# module(load="impstats" interval="60" format="cee" log.syslog="on")
# Then:
sudo grep -c 'monad-fwd' /var/log/syslog && echo "Queue stats present"
End-to-End Test

The goal: generate a log event on a source device, watch it traverse rsyslog, appear in Monad metrics, and land in Sentinel.

Step 1: Generate test log on ASA
! On the ASA CLI — generate a syslog event
logging message 199999 level 6
! Or simply make a minor config change and revert
Step 2: Verify log arrived at rsyslog
# On CHLXSYSLOG01
sudo tail -f /var/log/syslog | awk '/%ASA/{print; count++; if(count>=3) exit}'
Step 3: Verify log forwarded to Monad
# On your workstation — check Monad pipeline logs (allow 60s for propagation)
sleep 60 && curl -s -X GET \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/logs/pipelines/${PIPELINE_ID}" | \
  jq '.logs[:5]'
Step 4: Verify log in Sentinel (KQL)
// In Log Analytics workspace — Sentinel
Syslog
| where TimeGenerated > ago(10m)
| where SyslogMessage contains "%ASA-"
| take 10
Validation Checklist
# Check Command / Method Expected

1

rsyslog config valid

sudo rsyslogd -N1

No errors

2

rsyslog service running

systemctl is-active rsyslog

active

3

Queue spool directory exists

ls /var/spool/rsyslog/

Directory present

4

No persistent queue files

ls /var/spool/rsyslog/monad-fwd.*

No files (messages flowing)

5

Monad pipeline enabled

GET /v2/…​/pipelines/{id}

"enabled": true

6

Pipeline has input + output nodes

GET /v2/…​/pipelines/{id} nodes array

2 nodes minimum

7

Edge connects input to output

GET /v2/…​/pipelines/{id} edges array

1+ edges

8

No pipeline errors

GET /v1/…​/logs/pipelines/{id}

No "level": "error"

9

ASA logs visible in rsyslog

grep '%ASA' /var/log/syslog | tail -5

Recent ASA messages

10

Logs arriving in Sentinel

KQL: Syslog | where SyslogMessage contains "%ASA-"

Events present

Rollback

If the pipeline causes issues — disable forwarding without losing the configuration.

Disable rsyslog forwarding (on CHLXSYSLOG01)
# Comment out the forward, don't delete — you'll want it back
sudo sed -i 's/^action(type="omfwd"/# action(type="omfwd"/' /etc/rsyslog.d/90-monad-forward.conf
sudo systemctl restart rsyslog
Disable Monad pipeline (from workstation)
curl -s -X PATCH \
  -H "X-API-Key: ${MONAD_API_KEY}" \
  -H "Content-Type: application/json" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/pipelines/${PIPELINE_ID}" \
  -d '{"enabled": false}' | jq '{id, name, enabled}'

NAS Secondary Mount — Resync Diagnostics

Phase 0: Current State Assessment

Establish ground truth before touching anything.

List all NFS/CIFS mounts currently active
mount | awk '/nfs|cifs|smb/{print}'
Structured mount table — findmnt is the canonical tool
findmnt -t nfs,nfs4,cifs
What fstab expects vs what is mounted
awk '/nfs|cifs/' /etc/fstab
Reachability — confirm L3 path to NAS
ping -c 2 10.50.1.70
NFS exports advertised by the NAS
showmount -e 10.50.1.70
Secondary mount status — a hanging stat indicates a stale handle
# Replace <secondary-mount> with the actual mount point from findmnt output above
timeout 5 stat /mnt/nas/<secondary-mount> 2>&1
ls -la /mnt/nas/<secondary-mount>/ 2>&1
If stat hangs past the 5-second timeout, the NFS handle is stale. Proceed directly to Phase 2, Option A or B.
Phase 1: Diagnose the Failure
Kernel ring buffer — NFS errors surface here first
dmesg | awk '/nfs|NFS|stale/i'
NFS client-side statistics — retransmits and timeouts indicate network or server issues
nfsstat -c
RPC services on the NAS — confirms NFS daemon is responding
rpcinfo -p 10.50.1.70
systemd mount unit status (if using .mount units instead of fstab)
# Derive the unit name: mount path /mnt/nas/foo becomes mnt-nas-foo.mount
systemctl list-units --type=mount | awk '/nas/'
# Then check the specific unit
systemctl status <unit-name>.mount
journalctl -u <unit-name>.mount --no-pager -n 50
Check for stale file handles explicitly
# ESTALE (errno 116) in dmesg or strace confirms stale handle
dmesg | awk '/ESTALE|116|Stale file handle/'
Phase 2: Recovery Options

Work through these in order — least disruptive first.

Option A: Lazy unmount + remount (preserves open file handles gracefully)
# Verify before
findmnt /mnt/nas/<path>

# Change — lazy unmount detaches without blocking on busy files
sudo umount -l /mnt/nas/<path> && sudo mount /mnt/nas/<path>

# Verify after
findmnt /mnt/nas/<path>
ls -la /mnt/nas/<path>/
Option B: Force unmount (when lazy unmount insufficient)
# Verify before — check what processes hold the mount
sudo lsof +f -- /mnt/nas/<path> 2>/dev/null | awk 'NR<=10'

# Change
sudo umount -f /mnt/nas/<path>
sudo mount /mnt/nas/<path>

# Verify after
findmnt /mnt/nas/<path>
stat /mnt/nas/<path>
Force unmount (-f) can cause data loss in processes with open writes. Check lsof output before proceeding.
Option C: If rsync-based sync — check prior run and re-execute
# Check if rsync left a partial transfer
find /mnt/nas/<path> -name '.~tmp~' -o -name '*.partial' 2>/dev/null

# Dry-run first — see what would transfer
rsync -avz --dry-run --progress <source>/ /mnt/nas/<path>/

# Execute when dry-run looks correct
rsync -avz --progress <source>/ /mnt/nas/<path>/
Option D: NAS-side checks via Synology DSM
# SSH to NAS if enabled
ssh admin@10.50.1.70

# On the NAS: check volume health
cat /proc/mdstat
df -h

# Check NFS export config
cat /etc/exports

# Check shared folder permissions
ls -la /volume1/
Phase 3: Resync Verification

After recovery, confirm data integrity.

Compare file counts between primary and secondary
echo "--- Primary ---"
find /mnt/nas/<primary> -type f | wc -l
echo "--- Secondary ---"
find /mnt/nas/<secondary> -type f | wc -l
Deeper diff — identify missing or divergent files
diff <(find /mnt/nas/<primary> -type f -printf '%P\n' | sort) \
     <(find /mnt/nas/<secondary> -type f -printf '%P\n' | sort)
If rsync is the sync mechanism — full resync with checksum verification
rsync -avzc --progress /mnt/nas/<primary>/ /mnt/nas/<secondary>/
The -c flag forces checksum comparison instead of mod-time/size. Slower but catches silent corruption.
Phase 4: Prevention
systemd automount — mount on access, unmount on idle
# /etc/systemd/system/mnt-nas-secondary.automount
cat <<'UNIT'
[Unit]
Description=Automount NAS secondary share

[Automount]
Where=/mnt/nas/<secondary>
TimeoutIdleSec=300

[Install]
WantedBy=multi-user.target
UNIT
Health check script — cron or systemd timer
#!/usr/bin/env bash
# /usr/local/bin/nas-mount-check.sh
MOUNT="/mnt/nas/<secondary>"

if ! timeout 5 stat "$MOUNT" &>/dev/null; then
    logger -t nas-check "ALERT: $MOUNT stale or unreachable — attempting remount"
    umount -l "$MOUNT" 2>/dev/null
    mount "$MOUNT"
    if timeout 5 stat "$MOUNT" &>/dev/null; then
        logger -t nas-check "OK: $MOUNT remounted successfully"
    else
        logger -t nas-check "FAIL: $MOUNT remount failed — manual intervention required"
    fi
fi
Cron entry — check every 15 minutes
# Verify before
crontab -l 2>/dev/null | awk '/nas/'

# Add if absent
(crontab -l 2>/dev/null; echo '*/15 * * * * /usr/local/bin/nas-mount-check.sh') | crontab -

# Verify after
crontab -l | awk '/nas/'
autofs as alternative — kernel-level automount
# /etc/autofs/auto.master.d/nas.autofs
echo "/mnt/nas /etc/autofs/auto.nas --timeout=300" | sudo tee /etc/autofs/auto.master.d/nas.autofs

# /etc/autofs/auto.nas — indirect map
# <key>  <options>  <server>:<export>
# secondary  -fstype=nfs4,rw  10.50.1.70:/volume1/secondary

sudo systemctl enable --now autofs
sudo systemctl status autofs

TCP Clocks — Batch 2 Onboarding (2026-06-09)

Request

9 new TCP time clocks (Kaba replacements). New OUI: 40:AC:BD (batch 1 was 40:AC:8D).

MAC Address

40:AC:BD:00:95:AB

40:AC:BD:00:95:AD

40:AC:BD:00:95:AE

40:AC:BD:00:95:AF

40:AC:BD:00:95:B0

40:AC:BD:00:95:B1

40:AC:BD:00:96:04

40:AC:BD:00:96:05

40:AC:BD:00:96:06

ISE — Endpoints Added

All 9 MACs added to IoT_Onboard via ERS batch add (self-contained block in d001/projects/tcp-clocks/partials/queries-ers.adoc, line 177+). Static group assignment, description stamped with date and PM.

Issue Found — New Switch Not Configured for ISE
Clocks were added to ISE but the switch they connect to is not configured as a RADIUS client in ISE. New switch was deployed without 802.1X/MAB configuration.
Impact
  • Clocks will not authenticate — switch is not sending RADIUS requests to ISE

  • Endpoints sit in ISE but never get a session

  • No dACL enforcement, no VLAN assignment

Root Cause
  • New switch introduced without InfoSec coordination

  • No RADIUS/AAA config, no ISE network device entry

  • Switch was not part of the existing NAD (Network Access Device) inventory

Table 1. Required Actions
Step Action Status

1

Identify the new switch — hostname, IP, model, location

2

Add switch as Network Device in ISE (Administration → Network Resources → Network Devices)

3

Configure AAA on the switch — radius-server host, aaa new-model, aaa authentication dot1x, aaa authorization network

4

Configure switch ports for MAB — authentication order dot1x mab, authentication port-control auto, mab

5

Verify RADIUS connectivity — test aaa group radius from switch

6

Verify clock authentication — show access-session on switch, check ISE live logs

7

Validate clocks hitting Time clock ACL authZ rule with Timeclock_CM_dACL

Switch onboarding commands (template — fill in IP and shared secret)
! AAA configuration
aaa new-model
aaa authentication dot1x default group radius
aaa authorization network default group radius
aaa accounting dot1x default start-stop group radius

! RADIUS server
radius server ISE-PAN
 address ipv4 <ISE_PAN_IP> auth-port 1812 acct-port 1813
 key <SHARED_SECRET>

! Global 802.1X
dot1x system-auth-control

! Per-port MAB (for BMS/IoT devices)
interface range GigabitEthernet1/0/1-48
 switchport mode access
 authentication host-mode multi-auth
 authentication order dot1x mab
 authentication priority dot1x mab
 authentication port-control auto
 mab
 dot1x pae authenticator
 spanning-tree portfast
ISE Network Device entry
! Via ISE GUI:
! Administration → Network Resources → Network Devices → Add
!   Name: <switch-hostname>
!   IP: <switch-ip>
!   RADIUS Shared Secret: <same as switch config>
!   Network Device Group: Location > <building>, Type > Switch

! Or via ERS API:
ers_write POST "/networkdevice" '{
  "NetworkDevice": {
    "name": "<switch-hostname>",
    "authenticationSettings": {
      "radiusSharedSecret": "<SHARED_SECRET>",
      "enableKeyWrap": false
    },
    "NetworkDeviceIPList": [{
      "ipaddress": "<switch-ip>",
      "mask": 32
    }],
    "NetworkDeviceGroupList": [
      "Location#All Locations",
      "Device Type#All Device Types#Switch"
    ]
  }
}'
Lesson Learned

New network equipment deployments MUST include InfoSec notification for ISE NAD registration. Without RADIUS client config on the switch and a corresponding Network Device entry in ISE, 802.1X/MAB enforcement is bypassed entirely — devices get flat network access with no segmentation.

Architectus Digitalis — Your Universal Identity

What This Is

A passphrase-protected age key for everything outside your personal vault. Teaching, collaboration, file sharing, future projects — one key, one identity.

~/.age/
├── identities/
│   ├── personal.key               ← YOURS. UNTOUCHED. d001/encrypt-file/decrypt-file
│   └── personal.pub               ← YOURS. UNTOUCHED.
├── recipients/
│   └── self.txt                   ← YOURS. UNTOUCHED.
└── architectus-digitalis/         ← NEW — your universal key
    ├── architectus-digitalis.key  ← passphrase-protected private key
    ├── architectus-digitalis.pub  ← public key (share freely)
    ├── recipients-team.txt        ← who can decrypt team files
    └── notes.adoc.age             ← your encrypted notes
Step 1 — Create the Directory
Create with strict umask — files born restricted, no chmod-after needed
umask 077
mkdir -p ~/.age/architectus-digitalis
ls -la ~/.age/
Expected output
drwx------ architectus-digitalis
drwx------ identities
drwx------ recipients
Step 2 — Generate Key + Passphrase-Protect in One Pipeline

The private key NEVER touches disk as plaintext. age-keygen outputs the secret to stdout, which pipes directly into age -p (passphrase encryption). The public key prints to stderr (your terminal) — copy it from there.

Generate and passphrase-protect in one shot
umask 077
age-keygen | age -p -o ~/.age/architectus-digitalis/architectus-digitalis.key

You will see on your terminal (stderr):

# created: 2026-06-09T...
# public key: age1...YOUR_PUBLIC_KEY...

Copy that age1…​ line — you need it for Step 3.

Then age prompts you for a passphrase — TWICE. Choose something strong. This passphrase protects the key at rest.

The AGE-SECRET-KEY-1…​ line was piped directly into the encrypted file. It never appeared on screen, never hit disk, never entered terminal scrollback. This is by design.
Step 3 — Derive and Verify the Public Key

Extract the public key FROM the encrypted private key. This simultaneously verifies your passphrase works and the key file isn’t corrupt.

Derive public key (passphrase prompted)
age -d ~/.age/architectus-digitalis/architectus-digitalis.key \
  | age-keygen -y \
  > ~/.age/architectus-digitalis/architectus-digitalis.pub

cat ~/.age/architectus-digitalis/architectus-digitalis.pub

This should output age1…​ — the same public key you saw in Step 2. The secret key was decrypted in memory, fed to age-keygen -y which extracted the public key, and the secret was discarded. No plaintext key on disk, no plaintext key in terminal scrollback.

Step 4 — Create Your Notes File

An encrypted notes file for passphrase hints, collaborator log, and usage history.

Stage plaintext in /tmp (tmpfs on Arch — RAM only, never hits disk)
cat > /tmp/architectus-digitalis-notes.adoc << 'EOF'
= Architectus Digitalis — Identity Notes
:revdate: 2026-06-09

== Identity

[cols="1,3"]
|===
| Field | Value

| Key Name | Architectus Digitalis
| Created | 2026-06-09
| Owner | Evan Rosado
| Purpose | Universal — collaboration, teaching, file exchange, future projects
| Public Key | (see architectus-digitalis.pub)
| Passphrase Hint | (write YOUR hint here — NOT the actual passphrase)
|===

== Collaborators

[cols="1,2,2"]
|===
| Name | Public Key (age1...) | Added

| _first collaborator_ | _their key_ | _date_
|===

== Usage Log

[cols="1,3"]
|===
| Date | Action

| 2026-06-09 | Key generated. Passphrase set. Zero plaintext exposure.
|===

== Rules

1. NEVER share architectus-digitalis.key (the encrypted private key file)
2. NEVER type the passphrase into chat, email, or any logged system
3. Share architectus-digitalis.pub freely — it can only encrypt, not decrypt
4. Add collaborators by appending their age1... to recipients-team.txt
5. This key is SEPARATE from personal.key — different purpose, different trust boundary
6. Removing a collaborator from recipients-team.txt only affects FUTURE encryptions — they can still decrypt anything previously shared with them. If trust is broken, that's a key rotation event.

== Backup

The passphrase-protected .key file is safe to back up:
- Copy to gocryptfs vault or dsec-managed store
- Consider paper backup of the secret key (one line) in a physically secure location
- If the NVMe dies without a backup, every file encrypted to this key is unrecoverable
EOF
Encrypt notes with your public key
age -e -R ~/.age/architectus-digitalis/architectus-digitalis.pub \
  -o ~/.age/architectus-digitalis/notes.adoc.age \
  /tmp/architectus-digitalis-notes.adoc

# /tmp is tmpfs on Arch — rm suffices (shred is pointless on tmpfs)
rm /tmp/architectus-digitalis-notes.adoc
/tmp is tmpfs on Arch — plaintext lives in RAM only, never written to disk. Verify: findmnt /tmp | grep tmpfs. The one risk: if you don’t have encrypted swap (or zram), tmpfs pages can be swapped out. Check: swapon --show or zramctl.
Step 5 — Initialize Recipients File
Create the recipients file with yourself as first member
{
  echo "# Architectus Digitalis — Evan Rosado"
  cat ~/.age/architectus-digitalis/architectus-digitalis.pub
  echo ""
  echo "# Add collaborators below — one age1... public key per line"
} > ~/.age/architectus-digitalis/recipients-team.txt

cat ~/.age/architectus-digitalis/recipients-team.txt
Step 6 — Verify Everything
Confirm personal key is untouched
stat -c '%a %U %G' ~/.age/identities/personal.key
# Expected: 600 evanusmodestus evanusmodestus
Confirm d001 still works
d001 open tcp-clocks && d001 close tcp-clocks
# Should decrypt and re-encrypt normally
Test architectus encryption round-trip
echo "architectus test $(date)" > /tmp/arch-test.txt

# Encrypt with recipients
age -e -R ~/.age/architectus-digitalis/recipients-team.txt \
  -o /tmp/arch-test.txt.age /tmp/arch-test.txt

# Decrypt with architectus (passphrase prompted)
age -d -i <(age -d ~/.age/architectus-digitalis/architectus-digitalis.key) \
  -o /tmp/arch-test-decrypted.txt /tmp/arch-test.txt.age

# Compare
diff /tmp/arch-test.txt /tmp/arch-test-decrypted.txt && echo "✓ architectus works"

# Clean up (tmpfs — rm suffices)
rm /tmp/arch-test.txt /tmp/arch-test.txt.age /tmp/arch-test-decrypted.txt
Confirm architectus CANNOT decrypt personal files (test exit code, not string match)
echo "test" | age -e -R ~/.age/recipients/self.txt \
  | age -d -i <(age -d ~/.age/architectus-digitalis/architectus-digitalis.key) \
  >/dev/null 2>&1 \
  && echo "✗ PROBLEM: architectus decrypted a personal file" \
  || echo "✓ architectus correctly excluded from personal vault"
Step 7 — How to Use Daily

Convention: stage all plaintext in /tmp (tmpfs), never in $HOME. This eliminates the rm/shred question entirely.

Encrypt a file for the team
# Stage in /tmp, encrypt, send the .age file
cp ~/some-file.txt /tmp/
age -e -R ~/.age/architectus-digitalis/recipients-team.txt -o /tmp/some-file.txt.age /tmp/some-file.txt
rm /tmp/some-file.txt
# Send some-file.txt.age via email, Teams, USB — it's encrypted
Decrypt a file someone sent you
age -d -i <(age -d ~/.age/architectus-digitalis/architectus-digitalis.key) \
  -o /tmp/decrypted.txt received-file.age
# Passphrase prompted once — plaintext lands in tmpfs
Add a new collaborator
echo "# Friend Name" >> ~/.age/architectus-digitalis/recipients-team.txt
echo "age1theirpublickey..." >> ~/.age/architectus-digitalis/recipients-team.txt
Read your notes (nvim without swap/undo files leaking to disk)
age -d -i <(age -d ~/.age/architectus-digitalis/architectus-digitalis.key) \
  -o /tmp/notes.adoc ~/.age/architectus-digitalis/notes.adoc.age

nvim -n -i NONE /tmp/notes.adoc

# After editing, re-encrypt and clean
age -e -R ~/.age/architectus-digitalis/architectus-digitalis.pub \
  -o ~/.age/architectus-digitalis/notes.adoc.age /tmp/notes.adoc
rm /tmp/notes.adoc
nvim -n -i NONE disables swap files and viminfo — prevents Neovim from writing plaintext fragments to ~/.local/state/nvim/. Without this flag, your decrypted notes could persist in Neovim’s undo history on disk.

Backup

The passphrase-protected .key file is safe to copy — without the passphrase it’s useless.

# Copy to your gocryptfs vault or dsec-managed store
cp ~/.age/architectus-digitalis/architectus-digitalis.key /path/to/secure/backup/

# Consider: paper backup of the raw secret key (one line)
# Decrypt to terminal, write by hand, store physically
age -d ~/.age/architectus-digitalis/architectus-digitalis.key
# Copy the AGE-SECRET-KEY-1... line to paper. Store in safe/lockbox.
# This is your recovery path if the NVMe dies.
If the key file is lost AND you have no backup, every file encrypted to Architectus Digitalis is permanently unrecoverable. The passphrase cannot help without the key file.

Revocation

Removing a collaborator from recipients-team.txt only prevents them from decrypting future files. Everything previously encrypted while their key was in the recipients list remains decryptable by them — forever. The math doesn’t change retroactively.

If trust is actually broken (key compromised, collaborator hostile):

# 1. Generate a NEW key (repeat Step 2)
# 2. Update recipients-team.txt with your new public key
# 3. Remove the compromised collaborator
# 4. Notify remaining collaborators to update their copy
# 5. Re-encrypt ALL shared files with the new recipients
# 6. The OLD key and all files encrypted to it are considered burned

This is a key rotation event, not a recipients-file edit.

Final State
~/.age/architectus-digitalis/
├── architectus-digitalis.key     ← passphrase-protected (600)
├── architectus-digitalis.pub     ← public key — share freely (644)
├── recipients-team.txt           ← team access list (644)
└── notes.adoc.age                ← your encrypted notes (644)

No plaintext private key ever touched disk. Passphrase required for every use. Completely isolated from personal.key.

Commands of the day

Terminal Commands

Glob with NULLglob — suppress errors on no matches

setopt NULLglob (zsh) prevents "no matches found" errors when a glob pattern expands to nothing. Without it, ls *.xyz in an empty match throws an error. With it, the pattern silently expands to nothing.

List specific file types from Windows Downloads (WSL path)
setopt NULLglob
ls /mnt/c/Users/erosado/Downloads/*.{png,pdf,jpg,jpeg}
output
/mnt/c/Users/erosado/Downloads/abnormal-project-v2.pdf
/mnt/c/Users/erosado/Downloads/CHLA_Abnormal_ReadWrite_Permissions.pdf
/mnt/c/Users/erosado/Downloads/Configuring SCEP with Microsoft NDES for Chrome Enterprise (2).pdf
/mnt/c/Users/erosado/Downloads/guest-acl-update-2026-04-16.pdf
/mnt/c/Users/erosado/Downloads/image.png
/mnt/c/Users/erosado/Downloads/intune-cert-commands-2026-06-08-for-tony.pdf
bash equivalent is shopt -s nullglob. The zsh setopt variant is session-scoped — resets on new shell.
PowerShell — BMS File Discovery

Full search session in d001/projects/bms-controller-segmentation/partials/pwsh-search-commands-2026-06-09.adoc.

Recursive file search with extension + name filter
Get-ChildItem "C:\Users\erosado" -Recurse -File -ErrorAction SilentlyContinue |
Where-Object {
  $_.Extension -match '\.vsdx|\.pdf|\.xlsx|\.docx' -and
  $_.Name -match 'bms|johnson|metasys|niagara|jci|BAS'
} |
Sort-Object LastWriteTime -Descending |
Select-Object -First 30 FullName, LastWriteTime
Build Script Gotcha — partials vs pages

Always build from pages/, never from partials/ directly. Building a partial standalone causes Asciidoctor PDF table cell truncation errors — the partial’s content renders as one oversized cell.

# WRONG — 25 truncation errors, 1.1MB
./scripts/build-antora-page.sh docs/modules/ROOT/partials/worklog/daily-notes/2026-06-09.adoc pdf

# CORRECT — clean build, 2.4MB
./scripts/build-antora-page.sh docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-09.adoc pdf
ISE ERS — Batch Endpoint Add Pattern

Self-contained block in d001/projects/tcp-clocks/partials/queries-ers.adoc line 177+.

Pattern: resolve group → loop MACs → POST with duplicate guard
gid=$(ers "/endpointgroup?filter=name.EQ.${TARGET_GROUP}" | \
  jq -r '.SearchResult.resources[0].id // empty')

while read -r MAC; do
  exists=$(ers "/endpoint?filter=mac.EQ.${MAC}" | \
    jq -r '.SearchResult.resources[0].id // empty')
  [[ -n "$exists" ]] && echo "SKIP: ${MAC}" && continue
  # POST to /ers/config/endpoint with MAC, description, groupId
done << 'EOF'
<MAC list heredoc>
EOF
Issues experienced running the following build script
 ./scripts/build-antora-page.sh docs/modules/ROOT/partials/worklog/daily-notes/2026-06-09.adoc pdf --theme light-cyan 2>&1
→ Repo: domus-captures
→ Source: /home/evanusmodestus/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/partials/worklog/daily-notes/2026-06-09.adoc
→ Partials: /home/evanusmodestus/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/partials
→ Examples: /home/evanusmodestus/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/examples
→ Building 2026-06-09
========================================
Building: /tmp/antora-build-189671/2026-06-09.adoc
========================================
→ Building PDF...
asciidoctor: ERROR: the table cell on page 2 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 3 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 4 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 5 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 6 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 7 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 8 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 9 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 10 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 11 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 12 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 13 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 14 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 15 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 16 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 17 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 18 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 19 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 20 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 21 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 22 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 23 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 24 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
asciidoctor: ERROR: the table cell on page 25 has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page
✓ PDF: 1.1M

========================================
Build complete!
========================================
Output directory: /tmp/antora-build-189671/output

-rw-r--r-- 1 evanusmodestus evanusmodestus 1.1M Jun  9 13:25 2026-06-09.pdf
✓ Output: /home/evanusmodestus/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/partials/worklog/daily-notes/output/
-rw-r--r-- 1 evanusmodestus evanusmodestus 1.1M Jun  9 13:25 /home/evanusmodestus/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/partials/worklog/daily-notes/output/2026-06-09.pdf

Work (CHLA)

CHARGE TIME IN PEOPLESOFT - CRITICAL. Do this NOW before anything else.

Critical (P0)

Project Description Owner Status Due Blocker

Linux Research (Xianming Ding)

EAP-TLS for Linux workstations, dACL, UFW

Evan

BEHIND (72 days overdue)

02-24

Certificate "password required" - nmcli fix documented

iPSK Manager

Pre-shared key automation

Ben Castillo

BEHIND

 — 

DB replication issues

MSCHAPv2 Migration

Legacy auth deprecation — 6,227 devices, 5 waves. 6 batch SQL queries + 3-API endpoint profile script added (05-11). Report due.

Evan

25% — Report due, batch queries ready

05-30

Report to turn in

Research Segmentation

All endpoints to Untrusted VLAN

Evan

BLOCKED

 — 

CISO decision pending

Disaster Recovery

ISE DR scoping — dot1x closed mode = total blackout

Evan

Scoping

 — 

 — 

Mandiant Remediation

Copy 4/16 findings, Guest ACL lab, Q2 assessment

Evan

Active

 — 

 — 

SIEM QRadar → Sentinel

Full SIEM platform transition. Monad console error resolved 05-12. Secrets configured. Blocked on DCR creation (Rule ID + Stream Name). Azure private network policy unresolved.

Evan

Active — blocked on DCR

Q2 2026

Victor/Mauricio: create DCR, resolve Azure network policy

Abnormal Security

AI email platform — ESA cutover. CR assigned, CAB May 12 15:00. Implementation May 14 10:00.

Evan

Active — CAB today 15:00

05-14

Pre-CAB checklist: confirm Tyler, Jason, Sarah

High Priority (P1)

Project Description Owner Status Target

ISE 3.4 Migration

Upgrade from 3.2p9

Evan

Blocked — maintenance window needed

Q2 2026

Switch Upgrades

IOS-XE fleet update (C9300, 3560CX)

Evan

Pending

Q2 2026

Spikewell BYOD VPN

dACL SQL, AD group integration

Evan

Active

 — 

Strongline Gateway

MAC capture, Identity Group setup — 37 days aging

Evan

Active — David Rukiza assigned

 — 

Abnormal Security

AI email security platform research, ESA cutover timeline

Evan

Newly assigned

 — 

DMZ Migration

External services audit behind NetScaler

Evan

Audit phase

 — 

Firewall Audit (murus-portae)

EtherChannel query, prefilter, policy assignments

Evan

Scoping — ASA API creds needed

 — 

iPSK Manager HA

Server 2 config, TLS, SQL security audit

Evan

In progress

 — 

Sentinel KQL

Build proficiency, distinguish from team

Evan

Onboarding

 — 

VNC Blocking

Block and eliminate VNC protocol enterprise-wide

Evan

Active — Phase 0 (Discovery)

Mid-June 2026

Strategic (P2)

Project Description Owner Status

HHS Regulatory Compliance

New HHS security policies implementation

TBD

NOT STARTED

InfoSec Reporting Dashboard

PowerBI metrics for executives

TBD

NOT STARTED

EDR Migration (AMP → Defender)

Endpoint protection consolidation

TBD

NOT STARTED

Azure Legacy Migration

Modern landing zone

Team

In Progress

ChromeOS EAP-TLS

SCEP + Victor, Paul testing

Victor

In Progress

P0 — Critical / Blocking

Security & Compliance

  • ISE 3.2 Patch 10 upgrade — CVE-2026-20147 CVSS 9.9 / CVE-2026-20148. Propose maintenance window once patch confirmed on software.cisco.com.

  • ISE Advisory sa-ise-rce-traversal-8bYndVrZ — check Patch 10 availability

  • Mandiant Remediation — findings status tracked. Working session prep + defensive posture documented (comms-2026-04-24). Copy 4/16 updates into Excel at work. Guest ACL lockdown (WIR-M-01) pending lab validation. appendix-todos updated with MSCHAPv2 milestones.

  • Guest ACL update — guest redirect ACL work needed. Lab validate GUEST_CWA_REDIRECT_MAX_SECURITY in d000, then joint CR with NE. On today’s task list.

  • Disaster Recovery & Downtime Procedures — ISE top priority (dot1x closed mode = SPOF for network access)

    • ISE DR: Document failover sequence — PAN, MnT, PSN priority order

    • ISE DR: RADIUS dead-server detection on WLCs/switches — critical-auth VLAN fallback

    • ISE DR: Backup/restore procedures — scheduled config backups, tested restores

    • FTD/FMC DR: FMC loss = no policy management

    • Network DR: Core/distribution switch failure, STP reconvergence, HSRP failover

    • Document RTO/RPO per system

SIEM Migration (QRadar → Sentinel)

  • SIEM QRadar → Sentinel Migration — LEAD ROLE. 4 collection iterations (Apr 16, 17, 17-streamlined, 20-streamlined). Python chart pipeline built (qradar-charts.py). Migration XLSX generated. Verification pending. Comms sent Apr 23.

    • d001 artifacts: 8 JSON exports, 2 CSV inventories, migration XLSX, top5 source SVG/PNG, verification doc

    • Dependency: Monad pipeline for log source transition

    • Dependency: Sentinel KQL proficiency for query migration

  • Monad Pipeline Evaluation (origin: 2026-03-11) — lead role. Console error RESOLVED 05-12. 06-09: Architecture decision — rsyslog (CHLXSYSLOG01) as collection tier → Monad → Sentinel. ISE lab → rsyslog → Monad 6-step execution guide created with 10 API calls. ASA lab logs already flowing through rsyslog. DCR still needed — Victor + Mauricio.

  • Sentinel KQL — build proficiency, distinguish from team. Azure portal access acquired.

  • QRadar log source report — run AQL queries, fetch JSON, generate Python Excel

Active Deployments & Migrations

  • MSCHAPv2 Migration — 6-sheet Standard Report ready. Migration window 05-04 to 05-30 CLOSED. Confirm final report status and next steps with team. 6,227 MSCHAPv2 devices, 14,249 EAP-TLS/TEAP (70% migrated).

  • MSCHAPv2 weekly cadence — recurring Wednesday call established (first 04-22). Completed 2026-04-22.

  • MSCHAPv2 ownership matrix — sent in scoping email 4/24 with manager callouts (@Albert, @John). Completed 2026-04-24.

  • TCP Clocks deployment — Batch 1: 7 clocks validated (OUI 40:AC:8D). Batch 2 (06-09): 9 new MACs (OUI 40:AC:BD) added via ERS. 1 one-off reassigned. New switch deployed without RADIUS/AAA — clocks can’t authenticate. Switch onboarding template + 3 validation queries documented. ERS queries self-contained with ers function.

  • SRT Research VLAN — confirm roles with Tony Sun: Tony implementor, Evan tester. CAB approved 04-21.

  • Downtime Computers enforcement — draft ISE AuthZ rule: medigate_724 + Wireless = DenyAccess. Separate CR. d001: DC queries, audit CSVs (v1-v3), wireless violations report delivered 04-21.

  • Enterprise Linux 802.1X — standardize Shahab/Ding deployment (CISO priority). Overdue since 02-24. Blocked by nmcli cert fix.

  • Abnormal Security — CR-2026-05-07. Implemented 05-13. 06-09 update: Full policy review — 20-section EOP validation commands rebuilt, Hoxhunt SCL-1 investigation (intentional bypass confirmed), sclizer junk folder triage (~800 emails), Outlook reactions audit added, Connect-ExchangeOnline msalruntime fix documented. ESA migration expansion in progress — priority to move off ESA to full environment.

    • Team: Cox/William, Landeros/Jason, Rosado/Evan, Naranjo/Mauricio, Sandoval/Carlos

  • ASA VPN: Okta RADIUS → Entra SAML — (NEW 06-09) 5-phase migration plan built. ASA baseline captured (2 tunnel groups: CHLA_CORPORATE_USERS, CHLA_BYOD_USERS). 6 ISE policy screenshots. Tony Sun (ASA), Justin Halbmann (Entra/Okta), Evan (ISE). VPN cert expires 07-28. PDF deliverable ready. Share with team this week.

Tube System Upgrade (NEW — 06-01)

  • Tube System Upgrade — iTrack 3528165. 15x 10" TS stations need MAC addresses added to ISE identity group IoT_Onboard. MACs received from vendor (C8:1A:FE:20:xx:xx series). Station list spans ICU (CTICU, PICU, BMT, NICU, NICCU), ED, Surgery, Trauma, Pharmacy. Vendor contact: John Genest. Rationale: manufacturer no longer supports current system; failure risks delayed/missed patient care.

BMS Controller Segmentation (MIGRATED — 06-09)

  • BMS Controller Segmentation — Full migration from Principia LaTeX to d001. 12 partials, 5 Mermaid diagrams, 4 legacy PDFs, ISE screenshots. d001 open bms-controller. Completed 2026-06-09.

BMS Device Inventory (NEW — 04-24)

  • BMS Device Inventory — 72 devices discovered across 37 switches (04-24). Profile-driven architecture (Claroty/Medigate). 16 queries built. Phase 0 complete. Next: cross-reference with Visio diagrams, classify by function, begin D2 diagrams. Cleanup: delete 4 orphaned test groups, migrate 4 retire-dACL devices, investigate 3 null-profile devices.

VNC Blocking (NEW — 05-11)

  • VNC Blocking — block and eliminate VNC enterprise-wide. Due mid-June 2026. Phase 0: discovery. January AQL query baseline to incorporate. Cross-reference BMS inventory for VNC-capable devices.

Investigations & Audits

  • Murus Portae (WAF) — Phase 0 discovery in progress. FMC cert expired. d001: DMZ NetScaler WAF investigation, zone map, architecture D2 diagrams (v1+v2 SVGs), FMC REST API reference guide, ops script. FMC API returning zero ACP rules — under investigation.

  • Firewall audit — FMC discovery inventory done (d001: fmc-discovery-2026-04-16). EtherChannel query, prefilter, policy assignments pending.

  • IoT Dr. Kim devices — RECURRING. All 4 MACs validated in IoT_iPSK_VLAN1620_Misc (04-24). v2 validation queries built with 7 deep analysis queries (group flapping, credential leakage, profile drift, NAS tracking, remediation timeline, deny audit, OUI scan). Revalidate — confirm no flapping since 04-24.

  • IoT device validation queries — v2 created with partials architecture, 16 queries across ERS/MnT/DataConnect/FMC. Completed 2026-04-24.

Stale Blockers (carried via carryover tracker)

  • k3s NAT verification — rule 170, 10.42.0.0/16 pod network (origin: 2026-03-09). 92 days. Blocks Wazuh indexer recovery → blocks SIEM visibility. Decide: test or defer to Q3.

  • Strongline Gateway VLAN fix — 8 devices wrong identity group (origin: 2026-03-16). 85 days. David Rukiza assigned — follow up on status.

Administrative

  • PeopleSoft — track time for current week

  • iTrack tickets — close open tickets

  • KQL library — build initial queries in codex + d001

  • Linux Research project — finalize and review

  • Tax filing 2025 (MFJ) — see encrypted case file in data/d000/personal/ for details and action items

P1 — Important

  • MSCHAPv2 action-item tracker — owner/status/next-steps per workstream

  • ISE admin MFA enforcement — recommendation tied to advisory (interim control pending Patch 10)

  • DMZ Migration — external services audit behind NetScaler. Linked to Murus Portae investigation.

  • Vocera/Wyse iTrack RCA — complete root cause report

  • GCC ISE Support — 3/4 nodes restored, PSN-04 deferred

  • Wazuh indexer recovery — blocked by k3s NAT (origin: 2026-03-09)

  • Vocera EAP-TLS Supplicant Fix (origin: 2026-03-12)

  • iPSK Manager HA — blocked by DB replication (Ben Castillo)

  • ISE 3.4 Migration — depends on Patch 10 completion first

  • Git history scrub — murus-portae-output.md + ise-analytics CSVs

  • Encrypt prep-cmds-2026-04-15.adoc — plaintext committed to git

  • ISE MnT Messaging Service — enable UDP syslog delivery (maintenance window needed)

Infrastructure (Personal)

  • Borg backups — test and validate on ALL systems (Razer, P16g, vault-01, bind-01, kvm-01, kvm-02)

  • Borg — verify backup script paths updated from dotfiles-optimus to dots-quantum

  • Borg — create initial archive for ThinkPad P16g if none exists

  • Libvirt VLAN hook debug on both KVMs

  • Te1/0/2 cable replacement and re-test

  • Vault Raft cluster — verify vault-01 rejoined

  • Fix EAP-TLS keyring/secrets issue on Razer workstation

Completed (confirmed — do not delete, archive only)

  • CR-2026-04-15 SRT Research VLAN — submitted to iTrack. Completed 2026-04-15.

  • CAB presentation 4/21 — SRT Research VLAN 233 → CHLA-Research. APPROVED. Completed 2026-04-21.

  • Downtime Computers wireless audit — 45 computers, 16 violating, v3 report delivered. Completed 2026-04-21.

  • Git identity fix — dots-quantum/git/.gitconfig email corrected. Completed 2026-04-21.

  • MSCHAPv2 10:30 meeting — next steps + ACL coordination. Completed 2026-04-17.

Service Requests (SR)

SR# Request Requestor Opened Status

3508542

Zoll cards connection issue

STALE — verify in iTrack

3508524

Disable dot1x on (2) network ports - 5th floor 3250 Wilshire (PXE-boot imaging issues)

STALE — verify in iTrack (issues persisted after disable)

3528165

Tube System Upgrade — 15 stations, MAC addresses for ISE IoT_Onboard identity group

Genest, John (vendor contact)

2026-06-01

NEW — MACs received, need ISE onboarding

Incidents (INC)

INC# Priority Description Opened SLA Status

1911859

Strongline Gateways in Miscellaneous Subnet

STALE — verify in iTrack (related to carryover P0)

Change Requests - Emergency (ECAB)

CR# Description Opened Scheduled Status

No emergency changes

Change Requests - Normal

CR# Description Opened Scheduled Status

No normal changes

Change Requests - Scheduled/Standard

CR# Description Opened Window Status

No scheduled changes

Change Requests - Root Cause / Post-Incident

CR# Description Related INC Opened Status

100451

Vocera Phones and Wyse devices went off network

STALE — verify in iTrack


Session Accomplishments (Claude Code)

Encrypted File Placement (from 06-08)

  • Decrypted 4 .age files, placed into d001 structure

  • intune-cert-commandsd001/investigations/2026-06-08-intune-cert-auth/

  • rsyslog-commands, rsyslog-commands-testd001/projects/siem-qradar-to-sentinel/scripts/

  • syslog-2026-06-08d001/projects/siem-qradar-to-sentinel/

Runbooks Created (5)

  • VNC Removal — Windows PowerShell discovery + uninstall, Linux apt/dnf, network device audit, nmap verification

  • rsyslog New Sources — ISE, switch, FMC → CHLXSYSLOG01 (source-side config + 3 rsyslog server-side configs)

  • Monad Pipeline — rsyslog omfwd forwarding with disk-assisted queue, 8-step Monad API pipeline creation

  • NAS Resync — stale handle detection, 4 recovery options, systemd automount prevention

  • TCP Clocks Batch 2 — 9 MACs added, switch not configured, action plan with AAA template

BMS Controller Segmentation — Full Migration from Principia

  • Source: Principia/02_Assets/PRJ-ISE-CHLA/profiling/BMS-Controller-Segmentation/ (LaTeX, doc ID 2025-ISE-088)

  • 12 AsciiDoc partials: executive summary, network architecture, ISE policy (77 device models), dACL (28 ACEs), Claroty integration (risk 63.8→42.4), troubleshooting, device inventory, quick reference, acronyms, issues, todos, search commands

  • 5 Mermaid diagrams: campus topology, MAB auth flow, protocol map, dACL enforcement, device onboarding

  • 4 legacy PDFs + 5 rendered SVGs in output/

  • All encrypted in data/d001/projects/bms-controller-segmentation/d001 open/close bms-controller

  • README with pull list for 15 remaining files on work machine (3 Visio sources, 3 spreadsheets, 3 BAS diagrams, policy docs)

Git Tree Audit & Cleanup

  • Created .graveyard/ — local archive directory (gitignored)

  • Removed from tracking: Alexandra Angulo resume (PDF+TXT), ISE analytics PDF, 2 generated worklog PDFs

  • Removed runtime state: .claude/scheduled_tasks.json, .claude/scheduled_tasks.lock

  • Added .mmd to data/ gitignore pattern

TCP Clocks Batch 2 — ISE ERS Operations

  • 9 new MACs (OUI 40:AC:BD) added to IoT_Onboard via ERS batch add with exists-check

  • Updated queries-ers.adoc: self-contained batch block (dsource + ers function + heredoc), 3 validation queries

  • Updated queries-dataconnect.adoc: widened all SQL filters for new OUI prefixes

  • Updated context.adoc: batch 2 OUI and status

  • Issue discovered: new switch deployed without RADIUS/AAA config — endpoints in ISE but can’t authenticate. Switch onboarding template documented.

rsyslog Architecture Decision

  • Confirmed rsyslog as collection tier in front of Monad (not sources → Monad direct)

  • Advantages: protocol normalization, disk-assisted queues (backpressure), edge filtering, single config point for new sources

  • Pattern matches enterprise standard: sources → collector (rsyslog) → pipeline (Monad) → SIEM (Sentinel)

ASA VPN Migration: Okta RADIUS → Entra ID SAML

  • Full migration plan built (PRJ-2026-06-asa-vpn-okta-to-entra)

  • ASA baseline captured: 2 tunnel groups (CHLA_CORPORATE_USERS, CHLA_BYOD_USERS), group policies, crypto/certs, DAP, active sessions

  • 6 ISE policy screenshots captured (BYOD + Corporate — policy set, authc rules, authz rules)

  • Entra app config guide: SAML URLs, claims, Conditional Access, cert import, ASA SAML IdP config

  • ISE role analysis: full before/after comparison — ISE keeps authorization, accounting, posture unchanged. Only authentication moves to SAML.

  • 5-phase timeline: Prep → Entra (Justin Halbmann) → ASA (Tony Sun) + ISE (Evan) → Cutover → Cleanup

  • Risk matrix: 6 risks including VPN SSL cert expiring 2026-07-28 (49 days)

  • 12-step migration checklist with owners

  • PDF built with light-cyan theme + thankful_eyes syntax highlighting

TCP Clocks — Batch 2 + Switch Discovery

  • 9 new MACs (OUI 40:AC:BD) added to ISE via ERS batch API

  • 1 one-off clock (40:AC:8D:00:94:10) reassigned to IoT_Onboard

  • Discovered new switch deployed without RADIUS/AAA config — clocks in ISE but can’t authenticate

  • ERS queries file reorganized: CRUD ops grouped, batch add self-contained with ers function, 3 validation queries added

  • DataConnect queries widened for new OUI prefixes

Abnormal Security / Email Policy Review

  • Hoxhunt phishing simulation investigation: SCL -1 bypass, intentional delivery confirmed

  • Connect-ExchangeOnline troubleshooting: msalruntime DLL fix documented

  • Policy review validation commands rebuilt: 20 sections (anti-spam, anti-phish, outbound, Safe Links, Safe Attachments, transport rules, malware, connection filter, tenant allow/block, connectors, message trace, historical search, quarantine, ATP report, audit log, pilot scoping, mailbox-level, Outlook reactions, ORCA)

  • sclizer junk folder investigation added (~800 emails in junk)

ISE Lab → rsyslog → Monad Pipeline

  • 6-step execution guide created in d001/projects/siem-qradar-to-sentinel

  • ISE syslog config (GUI steps + logging categories), rsyslog per-source config, Monad API pipeline creation (10 API calls), rsyslog omfwd forwarding, end-to-end validation

RAD Projects

  • New d001 project scaffolded: d001 open rad-projects

Dashboard & Reporting System

  • scripts/generate-dashboards.sh — 11 auto-generated SVG diagrams (repo map, productivity, Gantt, competency, SIEM, toolchain, infra, d001 landscape/timeline/API/education)

  • scripts/domus-report.py — Python COO reporting tool, 14 matplotlib charts (overview, projects, education, growth, files, competencies, analytics)

  • Analytics report: scatter + regression (Pearson r=0.077), commit heatmap (peak Thu 15:00), cumulative growth curve, Pareto 80/20 distribution

  • 12 domain-specific Graphviz diagrams: 4 network (ASA VPN, ISE deployment, SIEM detailed, home lab rack), 4 project (BMS policy, Abnormal mail flow, Monad internals, TCP clocks), 4 education (CISSP domains, literature tree, C curriculum, CLI mastery)

  • Makefile targets: make diagrams-dashboard, make report

  • Total: 37 visual assets (11 SVG + 14 PNG + 12 domain SVG)

YAML Substrate & Knowledge Graph

  • data/project-master.yaml — 37 CHLA projects, machine-readable twin to project-master.adoc

  • Fields: slug, priority, status, owners, depends_on, blocks, tools, d001/docs paths, milestones

  • 5 dependency chains for critical path analysis

  • Association engine populated: 107 new triples across 4 YAML files (chla projects, CLI tools, codex knowledge, homelab infrastructure)

  • CLAUDE.md updated with Project Graph section — 9 dependency chains for instant session context

  • Next: YAML→AsciiDoc generator, FastAPI layer, dynamic Gantt from YAML

Build Toolchain Improvements

  • Rouge style changed from monokai.sublime (brown) to thankful_eyes (navy-blue) in build-adoc.sh

  • light-cyan theme: dark terminal code blocks (#0F1E2D), generous padding, rounded corners, caption styling

  • Fixed: literal/listing blocks share code: theme key, [source,text] tagging for output blocks

  • Fixed: cisco-ioscisco_ios Rouge lexer name

  • Documented: build-antora-page.sh (Antora) vs build-adoc.sh (d001/standalone)

Commands & Patterns Learned

zsh: setopt NULLglob

  • Suppresses "no matches found" when glob expands to nothing

  • bash equivalent: shopt -s nullglob

  • Used for: ls /mnt/c/Users/erosado/Downloads/*.{png,pdf,jpg,jpeg}

PowerShell: Recursive file discovery with multi-filter

  • Get-ChildItem -Recurse -File | Where-Object { $.Extension -match '…​' -and $.Name -match '…​' }

  • Pipe to Sort-Object LastWriteTime -Descending for most recent first

  • Set-Clipboard to grab results for paste

ISE ERS: Self-contained batch operation pattern

  • dsourceers() function → resolve group UUID → loop with exists-check → POST

  • Heredoc for MAC list — no temp files needed

  • Exists-check prevents duplicates: ers "/endpoint?filter=mac.EQ.${MAC}" | jq '.SearchResult.resources[0].id // empty'

age encryption: d001 workflow discipline

  • d001 close only scans partials/ — files in scripts/, output/, certs/, config-snapshots/ need one-off encrypt-file

  • Must rm stale .age before encrypt-file — old .age silently overrides edits

  • .mmd files need explicit gitignore under data/

Git hygiene: .graveyard/ pattern

  • Local archive directory, gitignored — for files removed from tracking but preserved on disk

  • git mvgit rm --cached → file stays in .graveyard/, removed from index


Personal

In Progress

Project Description Status Notes

k3s Platform

Production k3s cluster on kvm-01

Active

Prometheus, Grafana, Wazuh deployed

Wazuh Archives

Enable archives indexing in Filebeat

Active

PVC fix pending

kvm-02 Hardware

Supermicro B deployment

Active

Hardware ready, RAM upgrade done

Planned

Project Description Target Blocked By

Vault HA (3-node)

vault-02, vault-03 on kvm-02

Q2 2026 (slipped from Q1)

kvm-02 deployment

k3s HA (3-node)

Control plane HA

Q2 2026 (slipped from Q1)

kvm-02 deployment

ArgoCD GitOps

k3s GitOps deployment

After k3s stable

 — 

MinIO S3

Object storage for k3s

After ArgoCD

 — 

Domus Inventory

Personal asset management (YAML + CLI + AsciiDoc)

Q2 2026

Schema approved

Active — Infrastructure

Task Details Priority Status Due

Wazuh agent deployment

Deploy agents to all infrastructure hosts

P2

Pending

After archives fix

k3s Platform

Production k3s cluster on kvm-01

P1

In Progress

 — 

Wazuh Archives

Enable archives indexing in Filebeat, PVC fix

P1

In Progress

 — 

kvm-02 Hardware

Supermicro B deployment, RAM upgrade done

P1

In Progress

 — 


Active — Security & Encryption

Task Details Priority Status Due

Configure 4th YubiKey

SSH FIDO2 keys

P1

TODO

 — 

Cold storage M-DISC backup

age-encrypted archives

P1

TODO

After YubiKey setup


Active — Development & Tools

Task Details Priority Status Due

netapi Commercialization

Go CLI rewrite with Cobra-style argument discovery, package for distribution

P0

Active

 — 

Ollama API Service

FastAPI (17 endpoints), productize — config audit, doc tools, runbook gen

P0

Active

 — 

Shell functions (fe, fec, fef)

File hunting helpers

P3

TODO

 — 


Active — Documentation

Task Details Priority Status Due

D2 Catppuccin Mocha styling

domus-* spoke repos (177 files total)

P3

In Progress

 — 


Active — Financial

Task Details Priority Status Due

Amazon order history import

Download CSV from Privacy Central → parse with awk → populate subscriptions tracker

P1

Waiting

Pending Amazon data export (requested 2026-04-04)


Active — Education

Task Details Priority Status Due

No active education tasks — see education trackers


Active — Personal & Life Admin

Task Details Priority Status Due

ThinkPad T16g Setup

Arch install, stow dotfiles, Ollama stack, netapi dev env

P0

Pending

 — 

P50 Arch to Ubuntu migration

CR-2026-03-12

P2

In Progress

 — 

X1 Carbon Ubuntu installs

2 laptops, LUKS encryption

P2

In Progress

 — 

P50 Steam Test

Test Flatpak Steam + apt cleanup of broken i386 packages

P3

Pending

 — 

Documentation Sites

Notes

Day-specific personal notes here.


Education

Claude Code Mastery

Resource Details Progress Status

Claude Code Full Course (4 hrs)

Nick Saraev - YouTube comprehensive course

26:49 / 4:00:00

IN PROGRESS

Claude Code Certification

Anthropic official certification (newly released)

Not started

GOAL

Skills Mastery (Critical)

Certification Deadlines

  • CISSP - July 12, 2026 (10-week plan active — Week 1)

  • RHCSA 9 - Q3 2026 (after CISSP)

  • LPIC-1 - Renewal required (blocks LPIC-2)

Spanish C1 Certification Goals

Certification Provider Target Status Strategy

SIELE C1

Instituto Cervantes / UNAM / Salamanca

Q2 2026

ACTIVE

Computer-based, faster results - take FIRST

DELE C1

Instituto Cervantes

Q3/Q4 2026

PLANNED

After SIELE success, harder exam

DELE C2

Instituto Cervantes

2027

FUTURE

Mastery level - requires extensive immersion

SIELE is computer-adaptive, results in 3 weeks. DELE is paper-based, results in 3-4 months. Do SIELE first to validate readiness.

Don Quijote Writing Practice - DELE C1/C2 Initiative

Method:

  1. Read chapter in original Spanish

  2. Write personal analysis/understanding en espanol

  3. AI review for grammar, vocabulary, register

  4. Build comprehensive understanding of literary elements

Today’s Study

  • Focus: CISSP (41 days to July 12 exam — schedule exam today 06-01), MSCHAPv2 migration wrap-up

  • Secondary: RHCSA curriculum, Spanish SIELE C1

  • CISSP — Security & Risk Management (continuing). Schedule exam this afternoon.

  • RHCSA — continue curriculum phase

  • Spanish — Don Quijote reading + analysis (DTLA study day)

  • MSCHAPv2 — migration window closed 05-30, review final report

Regex Training (CRITICAL)

  • Status: 52 days carried over (since 2026-03-16)

  • Priority: After PeopleSoft, before Quijote

  • Session: Character classes, word boundaries


Infrastructure

Documentation Sites

Site URL Status Actions Needed

Domus Digitalis

docs.domusdigitalis.dev

Active

Validate, harden, improve

Architectus

docs.architectus.dev

Active

Public portfolio site - maintain

HA Deployment Status

System Description Status Notes

VyOS HA

vyos-01 (kvm-01) + vyos-02 (kvm-02) with VRRP VIP

✅ COMPLETE

2026-03-07 - pfSense decommissioned

BIND DNS HA

bind-01 (kvm-01) + bind-02 (kvm-02) with AXFR

✅ COMPLETE

Zone transfer operational

Vault HA

Raft cluster (vault-01/02/03)

✅ COMPLETE

Integrated with PKI

Keycloak Rebuild

keycloak-01 corrupted, rebuild from scratch

🔄 NEXT

Priority P3 - SSO broken

FreeIPA HA

ipa-02 replica planned

📋 PLANNED

Linux auth redundancy

AD DC HA

home-dc02 replication

📋 PLANNED

Windows auth redundancy

iPSK Manager HA

ipsk-mgr-02 with MySQL replication

📋 PLANNED

PSK portal redundancy

ISE HA

PAN HA (ise-01 reconfigure)

⏳ DEFERRED

Wait until ise-02 stable

ISE 3.5 Migration

Upgrade path: 3.2p9 → 3.4 (P1) → 3.5 (target)

📋 PLANNED

After 3.4 Migration completes (Q2 2026)

Single Points of Failure (CRITICAL)

These systems have NO redundancy - outage impacts production.
System Impact if Down Mitigation

ISE (ise-02)

All 802.1X stops - wired and wireless auth fails

ise-01 reconfiguration deferred until ise-02 stable

Keycloak (keycloak-01)

SAML/OIDC SSO broken (ISE admin, Grafana, etc.)

NEXT PRIORITY - Rebuild runbook

FreeIPA (ipa-01)

Linux auth, sudo rules, HBAC fails

ipa-02 replica planned

AD DC (home-dc01)

Windows auth, Kerberos, GPO fails

home-dc02 replica planned

iPSK Manager

Self-service PSK portal unavailable

ipsk-mgr-02 with MySQL replication planned

Validation Tasks

Task Details Status

docs.domusdigitalis.dev validation

Test all cross-references, search, rendering

TODO

docs.domusdigitalis.dev hardening

HTTPS, CSP headers, security review

TODO

docs.architectus.dev validation

Public site content review

TODO

Hub-spoke sync verification

All components building correctly

Ongoing


Quick Commands

Git & GitHub CLI

create GitHub repo from existing local repo
gh repo create <name> --private --source . --remote origin --push
clone a forked repo into a specific directory
gh repo clone EvanusModestus/PowerShell ~/atelier/_projects/work/PowerShell
gh repo clone defaults to SSH. If key is passphrase-protected, load agent first: eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_github
cross-repo commit search — all domus repos on a specific date
for repo in ~/atelier/_bibliotheca/domus-*/ ~/atelier/_projects/personal/domus-*/; do
  [ -d "$repo/.git" ] || continue
  name=$(basename "$repo")
  git -C "$repo" log --since="2026-04-06" --until="2026-04-07" --format="%h %aI %s" 2>/dev/null |
    awk -v r="$name" '{print r, $0}'
done
commit history touching only today’s modified files
git log --oneline -- $(find . -name "*.adoc" -type f -newermt "$(date +%F)")
unstage a file without losing changes
git restore --staged data/d001/api/ise-dataconnect/output/output-2026-04-24

Safe — removes from staging area only. Working tree is untouched. Use when you accidentally git add a plaintext or output file.

gh CLI — repo discovery and filtering

list repos by name pattern (domus/antora ecosystem)
gh repo list --limit 100 --json name,description \
  | jq -r '.[] | select(.name | test("domus|antora|asciidoc"; "i")) | "\(.name)\t\(.description)"'
top 20 most recently updated repos
gh repo list --limit 100 --json name,description,updatedAt \
  | jq -r 'sort_by(.updatedAt) | reverse | .[:20] | .[] | "\(.updatedAt[:10])\t\(.name)\t\(.description)"'
top 10 repos by disk usage
gh repo list --limit 100 --json name,diskUsage \
  | jq -r '.[] | "\(.diskUsage)\t\(.name)"' | sort -rn | head -10
clone a repo that’s not local yet
gh repo clone EvanusModestus/<repo-name> ~/atelier/_bibliotheca/<repo-name>

find & grep

files modified since midnight today (precise — not "last 24 hours")
find . -name "*.adoc" -type f -newermt "$(date +%F)" | sort
-mtime 0 means "last 24 hours", not "today". -newermt "$(date +%F)" compares against midnight — exact.
case-insensitive file search
find . -iname "*mschap*" -type f | sort
multiple name patterns with -o
find . -type f \( -iname "*ise*" -o -iname "*mschap*" \) | sort
same thing, single regex — fewer parens, extensible
find . -type f -iregex '.*\(ise\|mschap\).*'
exclude directories
find . -type f -iname "*meeting*" \
  -not -path "*/node_modules/*" \
  -not -path "*/.git/*" \
  -not -path "*/build/*"
recent drafts by modification time (newest first)
find .drafts -type f -printf '%T@ %Tc %p\n' | sort -rn | awk '{$1="";print}' | head -3
grep — know what you’re counting
grep -rl "pattern" . --include="*.adoc"         # file count (which files)
grep -rn "pattern" . --include="*.adoc"         # line matches (every occurrence)
grep -rc "pattern" . --include="*.adoc" | grep -v ':0$'  # match count per file
search with context — avoid opening the file
grep -rn -E 'git init|gh repo create' docs/ --include='*.adoc' -B2 -A2

Search codex by content — which files contain a command?

find all PowerShell files that use a specific cmdlet
find docs/modules/ROOT/examples/codex/powershell -type f -name "*.adoc" \
  -exec grep -l 'Get-Process\|Start-Process\|pipeline\|Where-Object' {} \;

Pattern: find -exec grep -l returns only filenames with matches — like grep -rl but with find’s `-type f -name filtering. Use \| for OR in grep basic regex. Swap the pattern for any cmdlet or keyword to locate coverage across the codex.

inventory a codex tool directory — count files per tier
find docs/modules/ROOT -name "powershell" -type d \
  -exec sh -c 'echo "$1: $(find "$1" -type f | wc -l) files"' _ {} \;
find orphaned examples (not included by any page)
for f in $(find docs/modules/ROOT/examples/codex/powershell -name "*.adoc" -type f); do
  base=$(basename "$f")
  dir_parent=$(basename $(dirname "$f"))
  grep -rq "$dir_parent/$base" docs/modules/ROOT/pages/codex/powershell/ \
    docs/modules/ROOT/examples/codex/powershell/*.adoc 2>/dev/null \
    || echo "ORPHAN: $f"
done

find → grep → open in nvim

find by path + content, open result in nvim
nvim $(find -path '*oauth*' -name '*.adoc' -type f \
  -exec grep -l 'timeout\|expire\|reconfig\|token' {} \;)

Command substitution $(…​) feeds all matches as arguments to nvim — opens every hit as a buffer. :bn/:bp to cycle, :ls to list. One file? Opens directly. Five files? All loaded, ready to navigate.

find by content across entire tree, open in nvim
nvim $(find docs/modules/ROOT -name '*.adoc' -type f \
  -exec grep -l 'token.*expire\|oauth.*refresh' {} \;)
open one at a time (sequential — -exec nvim per match)
find -path '*oauth*' -name '*.adoc' -type f \
  -exec grep -l 'timeout\|expire' {} \; \
  -exec nvim {} \;
Trailing \| in grep patterns matches empty string — every file matches. Always end with a term, not a pipe: 'timeout\|expire\|token' not 'timeout\|expire\|token\|'.

Trace Antora partial inclusion chains

who includes this partial? (one level up)
grep -rl 'commands/shell' docs/modules/ROOT/partials/
count all pages that include a partial
grep -rl 'quick-commands' docs/modules/ROOT | wc -l
full chain: partial → assembler → every page that uses it
file="commands/shell"
grep -rl "$file" docs/modules/ROOT/partials/ | while read f; do
  parent=$(basename "$f" .adoc)
  echo "$file -> $parent"
  grep -rl "$parent" docs/modules/ROOT/pages/ | while read p; do
    echo "  -> $(basename "$p")"
  done
done

Pattern: grep -rl finds which files contain the string. Chain two passes — first finds the assembler partial, second finds every page that includes it. Works for any partial in the Antora include hierarchy.

Multi-pattern file search — worklog partial discovery

brute force — one find per partial name
find docs/modules/ROOT -name "*urgent.adoc*" -type f
find docs/modules/ROOT -name "*morning.adoc*" -type f
consolidated — single find with regex (production approach)
find docs/modules/ROOT -type f -regextype posix-extended \
  -regex '.*(urgent|morning|work-chla|personal|education|infrastructure|quick-commands|related)\.adoc' \
  | sort

Pattern: -regextype posix-extended enables | alternation without escaping. One process, one sort — versus 8 separate finds. The sort deduplicates visually and groups by path.

pipeline alternative — find piped to grep
find docs/modules/ROOT -type f -name "*.adoc" \
  | grep -E 'urgent|morning|work-chla|personal|education|infrastructure|quick-commands|related'

Trade-off: the pipeline version is more readable but spawns two processes. The regex version is a single find — faster on large trees, same result.

Cross-repo literary term search — bibliotheca-wide discovery

When searching for a term across the entire _bibliotheca (multiple repos, mixed file types), these patterns escalate from narrow to broad.

1. Single repo — count matches per file
grep -rn --include='*.adoc' -c 'sanchuelo' . | grep -v ':0$'
2. Cross-repo — filenames only (all bibliotheca)
grep -rl --include='*.adoc' -i 'sanchuelo' ~/atelier/_bibliotheca/ | sort
3. Cross-repo with context — see the line in situ
grep -rn --include='*.adoc' -i -B1 -A1 'sanchuelo' ~/atelier/_bibliotheca/domus-captures/
4. Multi-filetype — .adoc + .txt (catches source texts)
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.txt' --include='*.adoc' | sort
5. Null-safe find + xargs — handles spaces in paths
find ~/atelier/_bibliotheca/ -type f \( -name '*.adoc' -o -name '*.txt' \) -print0 \
  | xargs -0 grep -li 'sanchuelo' | sort
6. Open all hits directly in nvim
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.adoc' --include='*.txt' | xargs nvim

Pattern escalation: #1 confirms the term exists and where. #2 expands to all repos. #3 shows context without opening files. #4 adds plain text sources (Quijote .txt originals). #5 is the safe version for automation. #6 opens everything for editing.

Trade-off: grep -r --include is faster for known file types. find | xargs grep is safer for paths with spaces and more extensible (add -name '*.md' etc.). For literary searches across the bibliotheca, #4 or #5 is usually the right starting point — the source texts are .txt, not .adoc.

Email thread analysis — extract people, dates, commitments, silence

who’s in the thread (@ mentions + From headers)
grep -P '(@\w+|^From:.*<)' comms.adoc
timeline — every date with context
grep -nP '\d{1,2}/\d{1,2}/\d{2,4}|20\d{2}-\d{2}-\d{2}' comms.adoc
commitments — who promised what
grep -niP '(I can |I will |I.ll |we will |we.ll )' comms.adoc
open questions and unknowns
grep -niP '(\?|need to confirm|need to validate|TBD|pending)' comms.adoc

comm — set difference (who hasn’t replied)

# All recipients
grep -oP '<\K[^>]+' comms.adoc | sort -u > /tmp/all-recipients

# All senders
grep -P '^From:' comms.adoc | grep -oP '<\K[^>]+' | sort -u > /tmp/replied

# Who's silent — follow-up targets
comm -23 /tmp/all-recipients /tmp/replied

comm -23 outputs lines only in file 1 (recipients not in senders). Requires sorted input. grep -oP '<\K[^>]+' uses PCRE lookbehind — match < but don’t include it, capture until >.

Sort find results by modification time (newest first)

find discovers files but has no sort. Chain -printf with sort to order by mtime.

awk '{print $2}' truncates filenames with spaces — Familia Romana_ Lingva…​ becomes Familia. Always use the null-safe or sub() variants below for real data.
epoch sort — space-safe (production version)
# Sort by mtime, strip epoch prefix — handles spaces in filenames
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T@ %p\n' | sort -rn | awk '{sub(/^[^ ]+ /,""); print}'

sub(/[ ]+ /,"") removes everything up to and including the first space (the epoch). {print $2} would split on every space — fatal for Familia Romana_ Lingva Latina.

human-readable timestamps alongside
# ISO 8601 timestamps — readable and lexicographically sortable
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T+ %p\n' | sort -r | head -20

%T+ renders YYYY-MM-DD+HH:MM:SS — no epoch math needed, still sorts correctly as text.

null-safe — the bulletproof version
# Null-delimited: survives any filename (newlines, quotes, unicode)
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T@\t%p\0' | sort -zrn | awk -v RS='\0' -F'\t' '{print $2}'

-printf '%T@\t%p\0' — tab separates epoch from path, null terminates. sort -z sorts null-delimited records. awk -v RS='\0' -F'\t' reads null-terminated, splits on tab — $2 is now the full path regardless of spaces.

stat fallback — portable (BSD/macOS)
# GNU stat equivalent — works where -printf is unavailable
find ~/Downloads -maxdepth 1 -name '*latin*' -exec stat --format='%Y %n' {} + | sort -rn | awk '{sub(/^[^ ]+ /,""); print}'

-exec …​ {} + batches all files into one stat call (faster than \;). On macOS, use stat -f '%m %N' instead of --format='%Y %n'.

File intelligence — size, type, duplicates, age

Beyond finding files — interrogating them.

top 10 largest files in a directory tree
# Size in bytes (-printf %s), human-readable via numfmt
find ~/Downloads -type f -printf '%s\t%p\n' | sort -rn | head -10 | numfmt --to=iec --field=1

numfmt --to=iec --field=1 converts the first field from bytes to K/M/G. sort -rn on raw bytes is exact — ls -lhS rounds and sometimes mis-sorts.

find duplicates by size (fast pre-filter before checksumming)
# Files sharing a byte count — likely duplicates (confirm with md5sum)
find ~/Downloads -type f -printf '%s %p\n' | awk '{seen[$1]++; files[$1]=files[$1] "\n  " $0} END {for (s in seen) if (seen[s]>1) print files[s]}'
find duplicates by content — definitive
# md5sum only files with duplicate sizes (two-pass: fast then precise)
find ~/Downloads -type f -printf '%s\n' | sort | uniq -d | while read -r size; do
  find ~/Downloads -type f -size "${size}c" -exec md5sum {} +
done | sort | uniq -w32 -D

Two-pass: first find duplicate sizes (cheap), then md5sum only those (expensive). uniq -w32 -D compares first 32 chars (the hash) and prints all duplicates.

file type census — what’s actually in this directory?
# Count files by MIME type (not extension — extensions lie)
find ~/Downloads -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn

file --mime-type -b reports actual content type. -b suppresses filename. A .pdf that’s really text/html is a failed download.

stale files — untouched for 30+ days
# Files not accessed in 30 days — candidates for cleanup
find ~/Downloads -maxdepth 1 -type f -atime +30 -printf '%A+ %s\t%p\n' | sort | numfmt --to=iec --field=2

-atime 30` = access time older than 30 days. `-printf '%A' shows last access. Useful for Downloads cleanup without deleting something you just renamed.

disk usage by subdirectory — sorted
# Which subdirectories consume the most space?
find . -maxdepth 1 -type d -exec du -sh {} + 2>/dev/null | sort -rh | head -20

Batch operations — rename, move, transform

rename all files — strip spaces, lowercase, normalize unicode
# Dry run — show what would change (remove echo to execute)
find ~/Downloads -maxdepth 1 -type f -name '* *' -print0 | while IFS= read -r -d '' f; do
  dir=$(dirname "$f")
  base=$(basename "$f" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
  echo mv "$f" "$dir/$base"
done

IFS= read -r -d '' — the holy trinity for null-safe filename reading. IFS= prevents whitespace trimming. -r prevents backslash interpretation. -d '' reads until null.

move files by extension into categorized subdirectories
# Sort Downloads chaos into folders by type
find ~/Downloads -maxdepth 1 -type f -print0 | while IFS= read -r -d '' f; do
  ext="${f##*.}"
  case "$ext" in
    pdf|epub)    dest="books" ;;
    jpg|png|svg) dest="images" ;;
    sh|py|rb)    dest="scripts" ;;
    *)           dest="other" ;;
  esac
  mkdir -p ~/Downloads/"$dest"
  echo mv "$f" ~/Downloads/"$dest"/
done

${f##.} — parameter expansion: strip longest match of . from front, leaving only the extension. No basename or awk needed.

batch convert epub → asciidoc (like your Cicero fetch script)
# Convert all epubs in a directory to asciidoc via pandoc
find . -name '*.epub' -type f -exec sh -c '
  for epub; do
    adoc="${epub%.epub}.adoc"
    pandoc -f epub -t asciidoc "$epub" -o "$adoc" \
      && printf "  → %s (%s lines)\n" "$adoc" "$(wc -l < "$adoc")" \
      || printf "  ✗ failed: %s\n" "$epub"
  done
' _ {} +

-exec sh -c '…​' _ {} + — batch mode. _ fills $0 (script name, discarded). All matched files become $1, $2, …​ iterated by for epub. One sh invocation, not one per file.

xargs power patterns

parallel processing — 4 cores
# Checksum all PDFs in parallel (4 processes)
find ~/Downloads -name '*.pdf' -print0 | xargs -0 -P4 md5sum

-P4 runs 4 md5sum processes simultaneously. -print0 | xargs -0 is the null-safe pipeline — no filename can break it.

batched execution — two arguments at a time
# Compare files pairwise with diff
find . -name '*.adoc' -print0 | xargs -0 -n2 diff --brief

-n2 feeds two arguments per invocation. Useful for pairwise comparisons, copy operations (-n2 with cp), or any command taking exactly two args.

placeholder — insert filename at specific position
# Backup every config file: cp <file> <file>.bak
find /etc -maxdepth 1 -name '*.conf' -print0 | xargs -0 -I{} cp {} {}.bak

-I{} replaces {} with each filename. Slower than + batching (one cp per file) but necessary when the filename must appear in a specific position.

Process substitution — diff without temp files

compare two directory listings
# What files exist in study-A but not study-B?
diff <(find data/d000/education/ciceron-study -type f -name '*.adoc' | sort) \
     <(find data/d000/education/latin-study -type f -name '*.adoc' | sort)

<(cmd) creates a file descriptor from command output. diff sees two "files" — no temp files created, no cleanup needed.

compare file counts across directories
# Side-by-side: file type census of two directories
paste <(find dir1 -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn) \
      <(find dir2 -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn)

awk, sed, jq

awk — field extraction

print second field (whitespace-delimited)
awk '{print $2}' file.txt
custom delimiter — colon-separated (like /etc/passwd)
awk -F: '{print $1, $3}' /etc/passwd
extract JSON code blocks from AsciiDoc
awk '/\[source,json\]/{getline; if ($0 ~ /^----/) {p=1; next}} p && /^----/{p=0; next} p' file.adoc
field extraction with printf formatting
awk '{printf "%-30s %s\n", $1, $2}' file.txt

sed — stream editing

in-place replacement with verify-before/after
# Before
awk 'NR==73' /etc/ssh/sshd_config
# Change
sed -i '73s/#GSSAPIAuthentication no/GSSAPIAuthentication yes/' /etc/ssh/sshd_config
# After
awk 'NR==73' /etc/ssh/sshd_config
extract line range
sed -n '10,20p' file.txt

sed — line-targeted replacement (verify-before / change / verify-after)

the full pattern: locate → validate → change → verify
# 1. LOCATE: find the line number
grep -n 'adoc-pdf' zsh/.zshrc

# 2. VALIDATE: read the exact line before changing
awk 'NR==1760' zsh/.zshrc

# 3. CHANGE: target by line number — only hits that line
sed -i '1760s/alias adoc-pdf=/alias build-adoc=/' zsh/.zshrc

# 4. VERIFY: confirm change AND check for collateral
grep -n 'build-adoc\|adoc-pdf' zsh/.zshrc

Without the line number prefix (1760s/), sed replaces every match in the file — a shotgun. With it, surgical. The line number comes from grep -n.

multi-line verify — check two specific lines at once
awk 'NR==1218 || NR==1760' zsh/.zshrc
range extraction — NR for surgical reads from large files
# grep found the error at line 44164 — read 50 lines of context
awk 'NR>=44160 && NR<=44210' session-dump.adoc

No head | tail chains. No sed -n '44160,44210p'. One awk, two numbers.

grep -oP with \K — value extraction from key-value logs

extract just the value after a key (Perl regex)
# ISE syslog — extract failure reasons
grep -oP 'FailureReason=\K[^,;]+' /var/log/syslog | sort | uniq -c | sort -rn

# ISE — extract MAC addresses
grep -oP 'Calling-Station-ID=\K[0-9A-Fa-f:.-]+' /var/log/syslog | sort -u

# ISE — extract NAS IPs
grep -oP 'NAS-IP-Address=\K[0-9.]+' /var/log/syslog | sort -u

# ISE — extract device names
grep -oP 'NetworkDeviceName=\K[^,;]+' /var/log/syslog | sort -u

\K resets the match start — everything before \K is required context but excluded from output. [^,;]+ captures until the next delimiter. Pipe to sort -u for unique, sort | uniq -c | sort -rn for counted frequency.

pattern: grep -oP 'KEY=\KVALUE_REGEX' | sort pipeline
# Generic form — works for any key=value log format
grep -oP 'FIELD_NAME=\K[^,;]+' logfile | sort | uniq -c | sort -rn | head -20

jq — JSON processing

extract nested fields
curl -s localhost:8080/stats | jq '.stats.total_files'
filter array by property
jq '.results[] | select(.category == "standards")' response.json
transform to TSV for spreadsheets
jq -r '.[] | [.title, .path] | @tsv' response.json | column -t -s $'\t'
GitHub API + jq — commit history by path
gh api "repos/EvanusModestus/domus-captures/commits?path=docs/&per_page=10" |
  jq -r '.[] | "\(.commit.author.date[:10]) \(.sha[:7]) \(.commit.message | split("\n")[0])"'

Shell Patterns

xargs — when the next command reads arguments, not stdin

Next command reads…​ Use

stdin (awk, grep, wc, sort)

pipe directly

arguments (stat, rm, cp, nvim, git add)

xargs

copy today’s files to backup — -I{} placeholder
mkdir -p /tmp/adoc-backup-$(date +%F) && \
  find . -name "*.adoc" -type f -newermt "$(date +%F)" | \
  xargs -I{} cp {} /tmp/adoc-backup-$(date +%F)/
parallel validation — -P4 runs 4 at a time
find .drafts -name "*.adoc" -type f | xargs -P4 -I{} asciidoctor -o /dev/null {}
null-delimited pipeline — safe for filenames with spaces
find . -name "*.adoc" -type f -print0 | xargs -0 wc -l

Process substitution — <(cmd) treats output as a file

compare tracker state: yesterday vs today
diff <(grep '|' partials/trackers/work/adhoc/carryover.adoc | head -20) \
     <(git show HEAD~1:partials/trackers/work/adhoc/carryover.adoc | grep '|' | head -20)
files on disk vs files in nav — drift detection
diff <(find docs/modules/ROOT/pages/projects/chla/mschapv2-migration -name "*.adoc" -type f | sort) \
     <(grep -oP 'mschapv2-migration/[^[]+\.adoc' docs/modules/ROOT/nav.adoc | sort)

Command substitution — embed output as arguments

open most recently modified file in nvim
nvim "$(find data/ -name '*.adoc' -type f -printf '%T@ %p\n' | sort -rn | awk 'NR==1{print $2}')"
line count across a project
wc -l $(find docs/modules/ROOT -path '*mschapv2*' -name '*.adoc' -type f)

Conditional execution — capture, test, act

open matching files only if they exist
files=$(find .drafts -name 'in*' -type f) && [ -n "$files" ] && nvim $files
open files that contain unchecked items
files=$(grep -rl '\[ \]' .drafts/*.adoc) && [ -n "$files" ] && nvim $files
guard with grep -q — only act if pattern matches
grep -q 'TODO\|FIXME\|\[ \]' "$file" && nvim "$file"

Pattern: $(capture)[ -n ] tests non-empty → && only proceeds if true. grep -q is the idempotent guard — run repeatedly, only opens when there’s work.

Decrypt and open — find .age, decrypt, nvim in one shot

files=$(find . -name "*tcp-clock*.age" -type f) && \
  [ -n "$files" ] && echo "$files" | xargs -I{} decrypt-file {} && \
  nvim $(echo "$files" | sed 's/\.age$//')

Pattern: find .age only (never tries plaintext), sed derives the decrypted path, guard prevents empty nvim. Change the glob to match any project.

tee_clean — color on screen, clean text in file

tee_clean() {
  tee >(sed 's/\x1b\[[0-9;]*m//g' > "$1")
}

# Color output on terminal, stripped in file
jq -C '.' data.json | tee_clean output.json
xq -C '.' data.xml | tee_clean output.json

# Wrap a whole block
{
  echo "=== Summary ==="
  jq -C '.[] | .name' data.json
} | tee_clean summary.txt

The >(cmd) is process substitution — tee writes to stdout AND to the subshell pipe. sed strips ANSI escape sequences (\x1b\[[0-9;]*m) before they hit the file.

Dependency check — verify toolchain in one shot

for cmd in asciidoctor asciidoctor-pdf pandoc rouge d2 mmdc age; do
  printf "%-20s %s\n" "$cmd" "$(command -v $cmd >/dev/null 2>&1 && echo 'OK' || echo 'MISSING')"
done

Pattern: command -v checks if binary exists on PATH. >/dev/null 2>&1 suppresses output — we only care about exit code. Swap the tool list for any project’s dependencies.

printf safety — dashes as data, not options

wrong — printf treats --- as invalid option
printf '---\n\n'
right — %s format string treats --- as data
printf '%s\n\n' '---'

Kill stuck SSH sessions

Find established SSH connections
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2, $9}'
Kill all stuck SSH sessions to a specific host
lsof -i TCP -n -P | awk '/ssh.*kvm-01.*ESTABLISHED/ {print $2}' | sort -u | xargs kill
Kill ALL stuck SSH sessions
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2}' | sort -u | xargs kill

lsof -i TCP -n -P lists all TCP connections. awk filters for SSH + ESTABLISHED, prints only the PID ($2). sort -u deduplicates (multiple file descriptors per process). xargs kill sends SIGTERM to each.

File Descriptors & Redirection

The three file descriptors

FD Name Purpose

0

stdin

input to the command

1

stdout

normal output (valid results)

2

stderr

error messages

Split stdout and stderr into separate files

find / -name "*.conf" 1>results.txt 2>errors.txt

Suppress errors — 2>/dev/null

find / -name "*.conf" 2>/dev/null

Merge stderr into stdout — 2>&1

command 2>&1 | grep "pattern"

This sends both stdout and stderr through the pipe. Without 2>&1, only stdout reaches grep — errors print to the terminal and bypass the pipeline.

Heredoc patterns

multi-line input to a command
cat <<'EOF'
Line 1
Line 2
EOF
heredoc commit messages (quotes prevent variable expansion)
git commit -m "$(cat <<'EOF'
feat: add new feature

Multi-line description here.
EOF
)"

API & curl/jq

domus-api — Documentation System REST API

start the API server
cd ~/atelier/_projects/personal/domus-api && uv run uvicorn domus_api.main:app --host 0.0.0.0 --port 8080
health check
curl -s localhost:8080/ | jq
full-text search
curl -s 'localhost:8080/search?q=mandiant' | jq
search — extract path, title, match count
curl -s 'localhost:8080/search?q=mandiant' | jq '.results[] | {path, title, match_count}'
list pages by category
curl -s 'localhost:8080/pages?category=standards' | jq
all antora.yml attributes
curl -s localhost:8080/attributes | jq

GitHub API

cross-repo search via GitHub API
gh search code "vault seal" --owner EvanusModestus --json repository,path,textMatches |
  jq '.[] | {repo: .repository.full_name, file: .path, match: .textMatches[].fragment}'
count .adoc files in a repo via API
gh api 'repos/EvanusModestus/domus-captures/git/trees/main?recursive=1' |
  jq '[.tree[] | select(.path | endswith(".adoc"))] | length'

Domus Workflows

Read content from terminal (meeting-ready)

today’s worklog
bat docs/modules/ROOT/pages/2026/04/WRKLOG-$(date +%Y-%m-%d).adoc
current priorities
bat docs/modules/ROOT/partials/trackers/work/priorities/current.adoc
carryover backlog
bat docs/modules/ROOT/partials/trackers/work/adhoc/carryover.adoc
any project summary
bat docs/modules/ROOT/partials/projects/mandiant-remediation/summary.adoc

Search and discovery

find all files related to a topic
grep -rl "MSCHAPv2" docs/modules/ROOT/ --include="*.adoc" | sort
search codex entries
grep -rn "pattern" docs/modules/ROOT/partials/codex/ --include="*.adoc" -B1 -A3
list all worklogs for a month
ls -1 docs/modules/ROOT/pages/2026/04/WRKLOG-*.adoc

Tracker aging — calculate days from origin

how many days since a carryover item started
echo $(( ($(date +%s) - $(date -d "2026-03-09" +%s)) / 86400 ))

Encrypted data access (d001)

view encrypted file without disk write
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
  data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
  | bat --language asciidoc
project encryption dashboard
for d in data/d001/projects/*/; do
  total=$(find "$d" -type f | wc -l)
  plain=$(find "$d" -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | wc -l)
  printf "%-25s %s files  %s plaintext\n" "$(basename "$d")" "$total" "$plain"
done

d000 study builds

batch build all docs for a Quijote chapter range
for d in p1-cap-03{7,8,9}; do
  for f in data/d000/education/quijote-study/notas/$d/*.adoc; do
    d000 build "$d/$(basename "$f" .adoc)" html --variant light-cyan
  done
done
build a single study doc (use unique path fragment)
d000 build p1-cap-038/texto-anotado html --variant light-cyan
d000 build p1-cap-038/texto-anotado pdf --theme light-cyan
batch build PDFs for a chapter range
for d in p1-cap-03{7,8,9}; do
  for f in data/d000/education/quijote-study/notas/$d/*.adoc; do
    d000 build "$d/$(basename "$f" .adoc)" pdf --theme light-cyan
  done
done
open all rendered chapters at once
firefox data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.html &
firefox data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf &
print chapter PDFs (requires CUPS configured)
lp data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf
build LPL logic study (English / Spanish)
d000 build annotated-text pdf --theme light-cyan
d000 build lpl-study/notas/texto-anotado pdf --theme light-cyan
build Cicero study
d000 build de-oratore/libro-i/texto-anotado html --variant light-cyan

Available themes

PDF themes (--theme)
ls ~/atelier/_bibliotheca/domus-asciidoc-build/themes/pdf/ | sed 's/-theme\.yml//'
# base blue burgundy catppuccin creative dark don-quijote green
# learning light-cyan navy operations orange purple reference royal
HTML variants (--variant)
~/atelier/_bibliotheca/domus-asciidoc-build/docinfo/compose.sh --list
# light dark catppuccin royal light-cyan

ISE & Network Ops

ISE ERS API — endpoint CRUD

set credentials (session)
export ISE_HOST="{ise-ip}" ISE_USER="admin" ISE_PASS="$(gopass show -o ise/admin)"
list identity groups
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/identitygroup" \
  -H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.resources[].name'
check if endpoint exists by MAC
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/endpoint?filter=mac.EQ.AA:BB:CC:DD:EE:FF" \
  -H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.total'

Certificate inspection

view EAP-TLS client cert from local store
openssl x509 -in {cert-dir}/client.pem -text -noout | head -30
check cert expiry
openssl x509 -in {cert-dir}/client.pem -enddate -noout

Network diagnostics

check listening ports
ss -tlnp | grep -E ':{port-https}|:{port-ssh}|:{port-ldaps}'
test ISE connectivity
nc -zv {ise-ip} {ise-ers-port}
DNS resolution
dig {ise-hostname} +short

ISE eval rotation — backup & restore

backup from ISE CLI (when admin UI is license-locked)
# SSH to ISE
ssh admin@ise-02.inside.domusdigitalis.dev

# Verify NAS repo
show repository nas-01

# Get encryption key (on workstation)
dsource d000 dev/storage
echo $ISE_BACKUP_KEY

# Run backup
backup pre-rotation-2026-06 repository nas-01 ise-config encryption-key plain <KEY>
list backups on NAS
ssh admin@ise-02.inside.domusdigitalis.dev
show repository nas-01
restore to fresh ISE node
configure terminal
repository nas-01
  url nfs://10.50.1.70:/volume1/ise_backups
exit

restore <backup-filename> repository nas-01 encryption-key plain <KEY>

VyOS — VRRP & VLAN inspection

VRRP status and VIPs
show vrrp
show configuration commands | grep vrrp | grep 'address'
firewall zone membership
show configuration commands | grep 'firewall zone' | grep 'member'
DHCP leases and ARP
show dhcp server leases
show arp
full interface/VLAN map
show interfaces

CUPS printing — validation & setup

software validation
command -v lpstat && echo "CUPS present" || echo "CUPS not installed"
lpstat -r                                # scheduler running?
lpstat -p -d                             # printers + default
daemon lifecycle
sudo systemctl enable --now cups         # start + persist
printer discovery
lpinfo -v                                # available backends/URIs
lpinfo -m | grep -i <brand>             # available drivers
add printer and set default
sudo lpadmin -p <name> -v <uri> -m everywhere -E
lpoptions -d <name>
print
lp file.pdf                              # default printer
lp -d <name> -o sides=two-sided-long-edge file.pdf

PowerShell (from zsh)

All PowerShell commands run inside pwsh -NoLogo -Command '…​' from zsh. Running them bare fails — zsh interprets $, |, () as shell syntax.

Process management

top 5 processes by memory
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 5 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | Format-Table'
stop/start Teams
pwsh -NoLogo -Command 'Get-Process | Where-Object {$_.ProcessName -like "*teams*"} | Stop-Process'
pwsh -NoLogo -Command 'Start-Process "ms-teams"'

Export to JSON (pipe to jq)

always use -NoLogo when piping pwsh output to zsh tools
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 5 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | ConvertTo-Json' | jq '.'
Never pipe Format-Table into ConvertTo-Json — it produces layout metadata, not data. Select-Object first, then ConvertTo-Json.

Wi-Fi management (netsh)

force fresh network scan
netsh wlan disconnect interface="Wi-Fi"
netsh wlan show networks mode=bssid
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"

SSH from PowerShell

connect to homelab from Windows terminal
ssh evan@modestus-razer.inside.domusdigitalis.dev

WSL ↔ Windows — Cross-Environment Commands

From zsh (WSL) — control Windows
run any PowerShell command from zsh
pwsh -NoLogo -Command 'Get-Date'
run multi-line PowerShell from zsh (heredoc)
pwsh -NoLogo -Command "$(cat <<'PS'
$procs = Get-Process | Where-Object { $_.WorkingSet64 -gt 100MB }
$procs | Sort-Object WorkingSet64 -Descending |
  Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize
PS
)"
open a file in Windows from WSL
# Open in default Windows app
wslview /mnt/c/Users/erosado/Documents/report.pdf

# Open Explorer to current WSL directory
explorer.exe .

# Open specific Windows path
explorer.exe 'C:\Users\erosado\Downloads'
copy WSL output to Windows clipboard
# Pipe anything to Windows clipboard
cat file.txt | clip.exe

# Copy a command's output
pwsh -NoLogo -Command 'Get-TransportRule | Format-List Name, State' | clip.exe
access Windows files from WSL
# Windows C: drive is at /mnt/c
ls /mnt/c/Users/erosado/Downloads/

# Copy from Windows to WSL
cp /mnt/c/Users/erosado/Downloads/report.pdf ~/atelier/

# Watch a Windows directory for new files
find /mnt/c/Users/erosado/Downloads -maxdepth 1 -mmin -5 -type f -printf '%T+ %p\n' | sort -r
From PowerShell — control WSL
run a bash command from PowerShell
wsl -e bash -c 'grep -rn "Ghost-Sender" ~/atelier/_bibliotheca/domus-captures/docs/'
run a specific WSL command and capture output
$result = wsl -e bash -c 'git -C ~/atelier/_bibliotheca/domus-captures log --oneline -5'
$result
Process Management — Windows Side
top processes by memory — formatted table
pwsh -NoLogo -Command '
Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 20 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}},
    @{N="CPU(s)";E={[math]::Round($_.CPU,1)}},
    @{N="Handles";E={$_.HandleCount}} |
  Format-Table -AutoSize'
find a specific process
pwsh -NoLogo -Command 'Get-Process | Where-Object { $_.ProcessName -like "*teams*" } |
  Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize'
kill by name
pwsh -NoLogo -Command 'Stop-Process -Name "Teams" -Force -ErrorAction SilentlyContinue'
kill by PID
pwsh -NoLogo -Command 'Stop-Process -Id 12345 -Force'
what’s listening on a port (Windows equivalent of ss -tulnp)
pwsh -NoLogo -Command 'Get-NetTCPConnection -State Listen |
  Select-Object LocalAddress, LocalPort, OwningProcess,
    @{N="Process";E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
  Sort-Object LocalPort | Format-Table -AutoSize'
specific port check
pwsh -NoLogo -Command 'Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue |
  Select-Object LocalAddress, LocalPort, RemoteAddress, State,
    @{N="Process";E={(Get-Process -Id $_.OwningProcess).ProcessName}}'
Services — Windows Side
list running services
pwsh -NoLogo -Command 'Get-Service | Where-Object { $_.Status -eq "Running" } |
  Sort-Object DisplayName | Format-Table Name, DisplayName, Status -AutoSize'
check a specific service
pwsh -NoLogo -Command 'Get-Service -Name "WinRM" | Format-List Name, DisplayName, Status, StartType'
restart a service
Restart-Service -Name "WinRM" -Force
System Info — Quick Health from zsh
one-shot Windows system summary
pwsh -NoLogo -Command '
Write-Host "=== Windows System ===" -ForegroundColor Cyan
Write-Host "Hostname: $env:COMPUTERNAME"
Write-Host "User:     $env:USERNAME"
Write-Host "OS:       $((Get-CimInstance Win32_OperatingSystem).Caption)"
Write-Host "Uptime:   $((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime)"
Write-Host "RAM:      $([math]::Round((Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize/1MB))GB total, $([math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory/1MB))GB free"
Write-Host "CPU:      $((Get-CimInstance Win32_Processor).Name)"
Write-Host "Disk C:   $([math]::Round((Get-PSDrive C).Free/1GB))GB free of $([math]::Round(((Get-PSDrive C).Used + (Get-PSDrive C).Free)/1GB))GB"'
disk usage — all drives
pwsh -NoLogo -Command 'Get-PSDrive -PSProvider FileSystem |
  Select-Object Name, @{N="Used(GB)";E={[math]::Round($_.Used/1GB,1)}},
    @{N="Free(GB)";E={[math]::Round($_.Free/1GB,1)}},
    @{N="Total(GB)";E={[math]::Round(($_.Used+$_.Free)/1GB,1)}} |
  Format-Table -AutoSize'
Exchange Online — Connect from zsh
connect to Exchange Online (launches MFA prompt in Windows)
pwsh -NoLogo -Command 'Connect-ExchangeOnline -UserPrincipalName erosado@chla.usc.edu'
MFA prompt opens in the Windows browser. After auth, the session persists in the pwsh process. For multi-command sessions, start pwsh interactively instead of one-shot commands.
interactive PowerShell session from zsh (for Exchange, etc.)
pwsh -NoLogo
# Then inside pwsh:
# Connect-ExchangeOnline
# Get-TransportRule | Format-List Name, State
# exit
File Transfer Patterns
move files between WSL and Windows
# WSL → Windows Downloads
cp ~/atelier/_bibliotheca/domus-captures/output/report.pdf /mnt/c/Users/erosado/Downloads/

# Windows → WSL (glob)
cp /mnt/c/Users/erosado/Downloads/*.{png,pdf,jpg} ~/atelier/_staging/

# Bulk move with null safety
find /mnt/c/Users/erosado/Downloads -maxdepth 1 -name '*.pdf' -mmin -60 -print0 |
  xargs -0 -I{} cp {} ~/atelier/_staging/
watch Windows Downloads for new files (live)
inotifywait -m /mnt/c/Users/erosado/Downloads -e create -e moved_to |
  awk '{printf "%s  %s\n", strftime("%H:%M:%S"), $3}'
inotifywait requires inotify-tools. Install with sudo pacman -S inotify-tools if not present.

Security & Encryption

View encrypted files without writing to disk

pipe age decrypt to bat — nothing touches the filesystem
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
  data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
  | bat --language asciidoc --file-name "findings-status-2026-04-16.adoc"

Batch re-encrypt — brace expansion + loop

re-encrypt multiple project files
for f in data/d001/projects/mandiant-remediation/{findings-status,guest-acl-update,siem-report}-2026-04-16.adoc; do
  rm -f "${f}.age" && echo y | encrypt-file "$f"
done
Always rm -f the .age first. If you skip it, encrypt-file prompts about overwrite and may only delete the plaintext without re-encrypting.

Detect stale plaintext — files needing re-encryption

find plaintext newer than its .age counterpart
for f in data/d001/projects/*/*.adoc; do
  age="${f}.age"
  if [ -f "$f" ] && [ -f "$age" ]; then
    pt_mod=$(/usr/bin/stat -c'%Y' "$f")
    age_mod=$(/usr/bin/stat -c'%Y' "$age")
    [ "$pt_mod" -gt "$age_mod" ] && echo "STALE: $f"
  fi
done

Secure delete — shred for sensitive plaintext

shred -u data/d001/projects/mandiant-remediation/man-report.txt
On SSD/NVMe, shred is less effective (wear leveling), but better than rm which only removes the directory entry.

Pre-push audit — find all unencrypted project files

find data/d001/projects -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | sort

System & Infrastructure

PipeWire audio validation

wpctl status                                    # PipeWire status
pactl list sinks short                          # list audio sinks
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga  # test default sink
journalctl -b --grep='sof|cs35l56' --no-pager | tail -20  # kernel audio firmware
cat /proc/asound/cards                          # ALSA sound cards

gopass — personal document management

gopass-personal-docs    # interactive entry creation
gopass-query bills      # list recurring bills with totals
gopass-query storage    # list storage units with gate codes
gopass-query export bills  # export category to JSON

Makefile — daily workflow

make new-day      # create today's worklog + update attributes
make serve        # build + local server (port 8000)
make              # build only
make sync-nav     # sync worklog nav entries
make update-index # rebuild monthly index

KVM — VM & ISO management

list VMs on a KVM host
ssh kvm-01 "sudo virsh list --all"
ssh kvm-02 "sudo virsh list --all"
find ISE ISOs across KVM hosts (case-insensitive glob)
ssh kvm-01 "ls -lh /mnt/nas/isos/*[Ii][Ss][Ee]* /var/lib/libvirt/images/*[Ii][Ss][Ee]* /mnt/onboard-ssd/isos/*[Ii][Ss][Ee]* 2>/dev/null"
ssh kvm-02 "ls -lh /mnt/nas/isos/*[Ii][Ss][Ee]* /mnt/ssd/libvirt/images/*[Ii][Ss][Ee]* 2>/dev/null"
console into a VM
sudo virsh console <vm-name>             # Escape: Ctrl+]
check NAS mount on KVM host
ssh kvm-01 "mount | grep nas; ls /mnt/"

Per-project file dashboard

per-project summary — total files vs unencrypted plaintext
for d in data/d001/projects/*/; do
  total=$(find "$d" -type f | wc -l)
  plain=$(find "$d" -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | wc -l)
  echo "$(basename "$d") | ${total} files | ${plain} plaintext"
done

USB-C / Thunderbolt Charging Diagnostics

Full evidence capture to file (one block, timestamped)
{
  echo "=== Power Supply ==="
  cat /sys/class/power_supply/*/status
  echo ""
  cat /sys/class/power_supply/*/type
  echo ""
  echo "=== UPower ==="
  upower -d | grep -E 'state|percentage|energy-rate|voltage'
  echo ""
  echo "=== dmesg (typec/thunderbolt/PD) ==="
  sudo dmesg | grep -iE 'typec|thunderbolt|ucsi|PD|power.delivery|charging' | tail -20
  echo ""
  echo "=== Pacman log (kernel/typec) ==="
  grep -iE 'thunderbolt|typec|ucsi|^.*upgraded linux ' /var/log/pacman.log | tail -20
} | tee /tmp/INC-$(date +%F)-usbc-charging.txt

Pattern: { } groups commands into a single stdout stream. tee writes to file AND displays on screen. Reusable for any multi-command evidence capture — change the commands inside, keep the structure.