WRKLOG-2026-06-24

Summary

Wednesday. Fill summary at end of day.

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

83

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

63

✅ 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

90

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

120

P0 - ACTIVE — ISE integration in progress

Guest Redirect ACL

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

2026-05-12

58

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

119

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

122

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

122

P0 - Blocked by k3s

Strongline Gateway VLAN fix

8 devices in wrong identity group (David Rukiza assigned)

2026-03-16

115

P0 - TODO

TCP Clocks deployment

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

2026-04-22

78

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

85

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

84

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

119

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

119

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: d000 open dissolutio. Close: d000 close dissolutio. 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

Infrastructure API reference — ISE + WLC + diagnostics, d000/d001

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/ise-api-reference-guide.adoc

MnT cert investigation — PMK cache root cause

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/mnt-cert-investigation.adoc

Claude Code memory system — architecture + session saves

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/claude-memory-system.adoc

SSH → DNS → DataConnect — tracing modestus-p50 IP

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/ssh-dns-discovery.adoc

ISE active session audit — 4 MACs, all MAB, workstation identification

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/ise-active-session-audit.adoc

Systemd timer inventory — 10 user timers, notification architecture, today’s alerts

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/cron-jobs.adoc

VNC Blocking — d001 project restructure, STD-001 compliance

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/vnc-blocking.adoc

Endpoint investigation — d001 production MAC 5C:FF:35:D8:BB:26

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/endpoint-investigation-d001.adoc

Palaestra — find + grep + xargs pipeline composition

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/find-grep-composition.adoc

CLI pattern — sed append after pattern match

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/sed-append-cli-pattern.adoc

tmux-quantum session restructure — 25 YAMLs audited, reduced to 18 active

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc

Palaestra — find -regex, POSIX basic vs extended, 5 drills with real paths

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/find-regex.adoc

Heredoc commit scripts — /tmp workflow, nested heredocs, multi-repo push

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/heredoc-commit-workflow.adoc

gopass v3 YAML audit — fd plumbing, pipe semantics, 371-entry inventory

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/gopass-v3-yaml-audit.adoc

MSCHAPv2 to cert migration — new d001 project, GPO delta analysis, cross-platform reporting

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/mschapv2-to-cert-migration.adoc

Arch vulnerability assessment — CVEs, system hardening, SSH/age pivot prevention, deep audit

■■■■■■■■■■ 10/10

worklog/daily-notes/2026-06/2026-06-24/arch-vulnerability-assessment.adoc

Security hardening deploy — work machine deployment runbook (dots-quantum + system configs)

□□□□□□□□□□ 0/10

worklog/daily-notes/2026-06/2026-06-24/security-hardening-deploy-work-machine.adoc

SSH Config → DNS → DataConnect — Tracing modestus-p50

SSH Config → DNS → DataConnect — Tracing modestus-p50

ssh modestus-p50 fails. No DNS A record in BIND. IP discovered via ISE DataConnect.

Diagnosis
SSH hostname unresolved
ssh -G modestus-p50 | awk '/^hostname/'
No A record in BIND
dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.90
DataConnect reveals the IP (10.50.10.107)
dc_query "
SELECT FRAMED_IP_ADDRESS, ENDPOINT_IP, USERNAME
FROM radius_authentications r
LEFT JOIN endpoints_data e ON r.calling_station_id = e.mac_address
WHERE r.calling_station_id = '14:F6:D8:7B:31:80'
ORDER BY r.timestamp_timezone DESC
FETCH FIRST 1 ROWS ONLY
" | jq '.[0] | {FRAMED_IP_ADDRESS, ENDPOINT_IP, USERNAME}'
Old SSH backup confirms the IP
grep -A2 'modestus-p50' ~/.ssh/config.bak.20260620
Fix — create DNS record
Check zone syntax
ssh bind-01 sudo named-checkzone inside.domusdigitalis.dev /var/named/inside.domusdigitalis.dev.zone
Edit zone — add A record, bump serial
ssh bind-01 sudo vim /var/named/inside.domusdigitalis.dev.zone
modestus-p50    IN  A  10.50.10.107
Verify syntax after edit
ssh bind-01 sudo named-checkzone inside.domusdigitalis.dev /var/named/inside.domusdigitalis.dev.zone
Reload zone
ssh bind-01 sudo rndc reload inside.domusdigitalis.dev
Verify primary
dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.90
Verify secondary
dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.91
Regenerate SSH host mappings
for name in kvm-0{1,2} nas-0{1,2} vyos-0{1,2} vyos-vip bind-0{1,2} ipa-0{1,2} \
  keycloak-0{1,2} ipsk-mgr-0{1,2} vault-0{1,2,3} certmgr-01 \
  home-dc0{1,2} 9800-wlc-0{1,2} wlc-0{1,2} k3s-master-0{1,2,3} \
  ise-0{1,2} gitea-01 3560cx-01 9300-01 ipmi-0{1,2} \
  wazuh wazuh-indexer wazuh-dashboard wazuh-workers wazuh-api \
  prometheus alertmanager grafana \
  modestus-{p16g,aw,razer} thinkpad-x1 modestus-p50 gabriel-workstation; do
  ip=$(dig +short "${name}.inside.domusdigitalis.dev" @10.50.1.90)
  [ -n "$ip" ] && printf "Host %s\n    HostName %s\n\n" "$name" "$ip"
done >| ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf
Verify SSH resolution
ssh -G modestus-p50 | awk '/^hostname|^user|^port/'
Re-encrypt
age -e -R ~/.age/recipients/self.txt \
  -o ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf.age \
  ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf
Advanced — command substitution + process substitution
Command substitution — capture IP from DataConnect into a variable
P50_IP=$(dc_query "
SELECT e.endpoint_ip
FROM endpoints_data e
WHERE e.mac_address = '14:F6:D8:7B:31:80'
FETCH FIRST 1 ROWS ONLY
" | jq -r '.[0].ENDPOINT_IP')
printf "modestus-p50 IP: %s\n" "$P50_IP"
Process substitution — diff zone state before vs after DNS edit
ZONE_BEFORE=$(dig +noall +answer axfr inside.domusdigitalis.dev @10.50.1.90 | \
  awk '$4=="A" {h=$1; sub(/\.$/,"",h); printf "%-45s %s\n", h, $5}' | sort -k2,2V)

After adding the A record and reloading:

diff <(echo "$ZONE_BEFORE") \
     <(dig +noall +answer axfr inside.domusdigitalis.dev @10.50.1.90 | \
       awk '$4=="A" {h=$1; sub(/\.$/,"",h); printf "%-45s %s\n", h, $5}' | sort -k2,2V)
Process substitution — verify primary-secondary agreement
paste <(dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.90) \
      <(dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.91) \
  | awk '{printf "primary=%-15s secondary=%s\n", $1, $2}'
Prevention — parse host list from key hierarchy dynamically
awk '/^Host /{for(i=2;i<=NF;i++) print $i}' \
  ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/20-d000-key-hierarchy.conf \
  | sort -u

ISE Active Session Audit — Endpoint Identification

ISE Active Session Audit — Endpoint Identification

Four active MACs on wireless — all MAB/iPSK, none are the Arch workstations.

Discovery
Session report — active MACs
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh')
Full profile — all active sessions via 3-API combined
dsource d000 dev/network/ise
source data/shared/scripts/ise-helpers.sh
for mac in 64:32:A8:C4:C7:19 A0:59:50:C9:92:EE 56:38:C0:55:C5:8F 24:FD:FA:04:A8:33; do
  MAC="$mac" . $(find data -name 'full-endpoint-profile.sh')
done
Results — active sessions (2026-06-24)
MAC Profile Group Auth

64:32:A8:C4:C7:19

Intel-Device

Profiled

MAB / iPSK

A0:59:50:C9:92:EE

Windows10-Workstation

Workstation

MAB / iPSK

56:38:C0:55:C5:8F

Unknown

Unknown

MAB / iPSK

24:FD:FA:04:A8:33

Unknown

Unknown

MAB / iPSK

All hit: Domus_MABDomus_IoT_WirelessDomus-IoT-iPSK

Actual workstations — EAP-TLS (not in active list)
MAC Host Cert CN Policy

E0:D5:5D:6C:E1:66

modestus-p16g wlan0

modestus-p16g.inside.domusdigitalis.dev

Domus_8021XDomus_Cert_Admin_P16g

14:F6:D8:7B:31:80

modestus-p16g wired

p50.inside.domusdigitalis.dev

Domus_8021XEAP_TLS_Permit

Verify workstation auth history
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, AUTHENTICATION_METHOD,
       AUTHENTICATION_PROTOCOL, CREDENTIAL_CHECK, PASSED,
       POLICY_SET_NAME, AUTHORIZATION_RULE, FRAMED_IP_ADDRESS
FROM radius_authentications
WHERE CALLING_STATION_ID = 'E0:D5:5D:6C:E1:66'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 5 ROWS ONLY
" | jq '.'
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, AUTHENTICATION_METHOD,
       AUTHENTICATION_PROTOCOL, CREDENTIAL_CHECK, PASSED,
       POLICY_SET_NAME, AUTHORIZATION_RULE, FRAMED_IP_ADDRESS
FROM radius_authentications
WHERE CALLING_STATION_ID = '14:F6:D8:7B:31:80'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 5 ROWS ONLY
" | jq '.'
Custom attributes — view current state
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') A0:59:50:C9:92:EE
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') E0:D5:5D:6C:E1:66
Update custom attributes
Write attributes file
jq -n '{OS:"Arch Linux", DevicePurpose:"Engineering Workstation"}' > /tmp/ep-attrs.json
Update endpoint — Linux-Workstations group (4833f280-fd8f-11f0-9bb2-fafc6167f873)
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') \
  E0:D5:5D:6C:E1:66 4833f280-fd8f-11f0-9bb2-fafc6167f873 "Evan - modestus-p16g - Arch Linux" /tmp/ep-attrs.json
Scripts created this session
endpoint-profile.sh — multi-MAC profiling
bash $(find data/d000/infra/ise-api-reference/scripts -maxdepth 1 -name 'endpoint-profile.sh') --active
update-endpoint.sh — ERS custom attribute updates
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') <MAC> [group-id] [description] [attrs-file]
The canonical endpoint profiling tool remains full-endpoint-profile.sh — use it for deep-dive single-MAC analysis. endpoint-profile.sh is useful for batch --active sweeps.
TODO
  • Identify Gabriel’s Arch workstation MAC

  • Populate custom attributes on both workstations

  • Investigate A0:59:50:C9:92:EE — profiled as Windows10-Workstation, likely Gabriel’s P50 on iPSK SSID

Infrastructure API Reference — ISE, WLC, Diagnostics

Infrastructure API Reference — ISE, WLC, Diagnostics

Complete reference for querying infrastructure APIs across environments. All scripts use dsource for credentials — swap d000 (lab) for d001 (production) and every command works identically.

Prerequisites
Load credentials — d000 (lab)
dsource d000 dev/network/ise
Load credentials — d001 (production)
dsource d001 dev/network/ise
Load helper functions (same for both environments)
source data/shared/scripts/ise-helpers.sh

Loaded functions: ers(), ers_write(), mnt(), dc_query()

Verify helpers are loaded
type ers mnt dc_query | head -3
Existing script inventory
ERS scripts
find data/d000/infra/ise-api-reference/scripts/ers -name '*.sh' -exec basename {} \; | sort
MnT scripts
find data/d000/infra/ise-api-reference/scripts/mnt -name '*.sh' -exec basename {} \; | sort
jq pattern scripts (MnT analysis)
find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name '*.sh' -exec basename {} \; | sort
OpenAPI scripts
find data/d000/infra/ise-api-reference/scripts/openapi -name '*.sh' -exec basename {} \; | sort
Shared helpers (ers, mnt, dc_query in one file)
cat data/shared/scripts/ise-helpers.sh
API Architecture
API Transport Output Helper

ERS

REST (port 9060) — JSON

jq

ers()

MnT

REST (port 443) — XML only

xq (XML → JSON)

mnt()

DataConnect

Oracle TCPS (port 2484) — JSON via Python

jq

dc_query()


1. Full Endpoint Profile — 3-API Combined

The canonical tool. Replicates ~95% of the ISE GUI "General Attributes" page.

Run for a single MAC
MAC="14:F6:D8:7B:31:80" . $(find data -name 'full-endpoint-profile.sh')

Output saved to /tmp/ise-endpoint-profile/ — one file per API surface:

<MAC>-dc.json              DataConnect 5-table JOIN
<MAC>-ers-identity.json    ERS endpoint + custom attributes
<MAC>-ers-group.json       ERS group name resolution
<MAC>-mnt-session.json     MnT active session
<MAC>-mnt-cert-tls.txt     MnT certificate + TLS details
<MAC>-mnt-auth-history.json MnT auth history (7 days)

2. ERS API — Endpoint Identity
List all endpoint identity groups
ers "/endpointgroup" | jq -r '.SearchResult.resources[] | [.name, .id] | @tsv'
Get endpoint by MAC
ers "/endpoint?filter=mac.EQ.14:F6:D8:7B:31:80" | jq '.SearchResult.resources[0]'
Get full endpoint detail (requires ID)
EP_ID=$(ers "/endpoint?filter=mac.EQ.14:F6:D8:7B:31:80" | jq -r '.SearchResult.resources[0].id')
ers "/endpoint/${EP_ID}" | jq '.ERSEndPoint | {mac, groupId, description, staticGroupAssignment, customAttributes: .customAttributes.customAttributes}'
Resolve group ID to name
GROUP_ID=$(ers "/endpoint/${EP_ID}" | jq -r '.ERSEndPoint.groupId')
ers "/endpointgroup/${GROUP_ID}" | jq '.EndPointGroup | {name, description}'
List all endpoints (paginated — uses ers_all from ise-common.sh)
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'list-endpoints.sh')
Update endpoint custom attributes
jq -n '{OS:"Arch Linux", DevicePurpose:"Workstation"}' > /tmp/ep-attrs.json
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') A0:59:50:C9:92:EE 4833f280-fd8f-11f0-9bb2-fafc6167f873 "Gabriel - modestus-p50" /tmp/ep-attrs.json

3. MnT API — Live Sessions and Auth History

MnT returns XML only (HTTP 406 on application/json). Always pipe through xq for JSON conversion.

Active session count
mnt "/Session/ActiveCount" | xq '.'
Active session list — all MACs
mnt "/Session/ActiveList" | xq '.activeList.activeSession[]?.calling_station_id'
Session detail by MAC
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq '.'
Session detail — key fields only
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq '.sessionParameters | {user_name, calling_station_id, framed_ip_address, nas_ip_address, endpoint_policy, acct_status_type, acct_session_id, auth_acs_timestamp}'
Certificate + TLS details (active sessions only)
other_attr_string is only populated during an active EAP-TLS session. Stopped sessions return null. The endpoint must be connected and authenticated.
Raw attribute string
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq -r '.sessionParameters.other_attr_string'
Parse into readable lines (:!: is ISE’s delimiter)
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n'
Filter for certificate and TLS fields only
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(Issuer|Subject|Days to Expiry|Serial Number|AKI|Extended Key Usage|Key Usage|TLSCipher|TLSVersion|TLSSignatureAlgorithms|TLSSupportedGroups)'
Filter for policy and authorization fields
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(ISEPolicySetName|AuthorizationPolicyMatchedRule|IdentityPolicyMatchedRule|EapChainingResult|AuthenticationStatus|AllowedProtocolMatchedRule|SelectedAuthenticationIdentityStores)'
Filter for profiler/manufacturer fields
mnt "/Session/MACAddress/14:F6:D8:7B:31:80" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(MFCInfoHardwareManufacturer|MFCInfoHardwareModel|MFCInfoEndpointType|OUI)'
Auth history by MAC

The MnT auth history lives in ISE memory — it is cleared on ISE restart and has a limited retention window. For persistent history, use DataConnect.

Last 24 hours, up to 20 records
mnt "/AuthStatus/MACAddress/14:F6:D8:7B:31:80/8640000/20/All" | xq '.'
Last 7 days
mnt "/AuthStatus/MACAddress/14:F6:D8:7B:31:80/604800/20/All" | xq '.'
Parsed — pass/fail, method, protocol, timestamp
mnt "/AuthStatus/MACAddress/14:F6:D8:7B:31:80/604800/20/All" | xq '.. | objects | select(.passed != null) | {passed: .passed."#text", failed: .failed."#text", user_name, calling_station_id, authentication_method, authentication_protocol, selected_azn_profiles, acs_timestamp}'
The "#text" accessor is required because xq represents XML text content as {"#text": "value"} when the element has attributes.
Session report — all active sessions enriched
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh')
JSON format (pipeable to jq patterns)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh') json
CoA — Change of Authorization (bounce session)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'coa-reauth.sh') 14:F6:D8:7B:31:80
CoA reauth fails if MnT thinks the session is stopped (results: false). In that case, clear the WLC PMK cache and reconnect from the client side. See mnt-cert-investigation.adoc for the full procedure.
jq analysis patterns (existing scripts)
Group active sessions by endpoint profile with counts
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'group-by-profile.sh')
Filter sessions — MAB only, dot1x only, missing IP, no accounting
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'select-filter.sh') mab
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'select-filter.sh') dot1x
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'select-filter.sh') missing-ip
Flag sessions by IP status and auth type
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'flag-missing-ip.sh')
Parse other_attr_string into key-value table
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'parse-attrs.sh') E0:D5:5D:6C:E1:66
Parse as JSON object (pipeable)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'parse-attrs.sh') E0:D5:5D:6C:E1:66 json
Reshape session into nested structure (identity/network/accounting/profiling)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'reshape-session.sh') E0:D5:5D:6C:E1:66

4. DataConnect — Oracle SQL (Persistent History)

DataConnect is the authoritative source for authentication history. Unlike MnT, it persists across ISE restarts and has full retention.

Connection test
dc_query "SELECT COUNT(*) AS total FROM radius_authentications" | jq '.[0].TOTAL'
Available tables
dc_query "SELECT table_name FROM user_tables ORDER BY table_name"
Discover columns for any table
dc_query "SELECT * FROM radius_authentications FETCH FIRST 1 ROWS ONLY" | jq '.[0] | keys'
dc_query "SELECT * FROM endpoints_data FETCH FIRST 1 ROWS ONLY" | jq '.[0] | keys'
dc_query "SELECT * FROM radius_accounting FETCH FIRST 1 ROWS ONLY" | jq '.[0] | keys'
Auth history for a MAC — full detail
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, CALLING_STATION_ID,
       NAS_IP_ADDRESS, NAS_PORT_ID, DEVICE_NAME,
       AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL,
       PASSED, FAILED, FAILURE_REASON,
       POLICY_SET_NAME, AUTHORIZATION_RULE, AUTHORIZATION_PROFILES,
       IDENTITY_GROUP, CREDENTIAL_CHECK, FRAMED_IP_ADDRESS
FROM radius_authentications
WHERE CALLING_STATION_ID = '14:F6:D8:7B:31:80'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 20 ROWS ONLY
" | jq '.'
Failed authentications — last 24 hours
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, CALLING_STATION_ID,
       NAS_IP_ADDRESS, AUTHENTICATION_METHOD, FAILURE_REASON,
       POLICY_SET_NAME
FROM radius_authentications
WHERE FAILED > 0
  AND TIMESTAMP_TIMEZONE > SYSTIMESTAMP - INTERVAL '24' HOUR
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 50 ROWS ONLY
" | jq '.'
Auth method breakdown — what protocols are in use
dc_query "
SELECT AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL, COUNT(*) AS CNT
FROM radius_authentications
GROUP BY AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL
ORDER BY CNT DESC
" | jq '.'
Policy set hit count
dc_query "
SELECT POLICY_SET_NAME, AUTHORIZATION_RULE, COUNT(*) AS CNT
FROM radius_authentications
GROUP BY POLICY_SET_NAME, AUTHORIZATION_RULE
ORDER BY CNT DESC
" | jq '.'
5-table JOIN — full endpoint context (canonical query)

This is the query inside full-endpoint-profile.sh. Use it directly when you need to customize the WHERE clause.

dc_query "
SELECT r.calling_station_id AS mac, r.username, r.identity_group,
       r.authentication_method, r.authentication_protocol,
       r.policy_set_name, r.authorization_rule, r.authorization_profiles,
       r.passed, r.failed, r.failure_reason,
       r.nas_ip_address, r.nas_port_id, r.nas_port_type,
       r.framed_ip_address, r.device_name, r.credential_check,
       r.identity_store, r.endpoint_profile,
       e.endpoint_policy AS profiler_policy,
       e.static_group_assignment, e.identity_group_id, e.endpoint_ip, e.hostname,
       g.name AS endpoint_group_name,
       n.name AS nad_name, n.profile_name AS nad_profile,
       a.acct_session_time, a.acct_status_type, a.acct_terminate_cause,
       a.acct_input_octets AS bytes_in, a.acct_output_octets AS bytes_out,
       r.timestamp_timezone
FROM radius_authentications r
LEFT JOIN endpoints_data e ON r.calling_station_id = e.mac_address
LEFT JOIN endpoint_identity_groups g ON e.identity_group_id = g.id
LEFT JOIN network_devices n ON r.nas_ip_address = SUBSTR(n.ip_mask, 1, INSTR(n.ip_mask, '/') - 1)
LEFT JOIN radius_accounting a ON r.audit_session_id = a.audit_session_id AND a.acct_status_type = 'Stop'
WHERE r.calling_station_id = '14:F6:D8:7B:31:80'
ORDER BY r.timestamp_timezone DESC
FETCH FIRST 1 ROWS ONLY
" | jq '.'
Endpoint profiler inventory
dc_query "
SELECT ENDPOINT_POLICY, COUNT(*) AS CNT
FROM endpoints
GROUP BY ENDPOINT_POLICY
ORDER BY CNT DESC
" | jq '.'
Accounting — active sessions with IP
dc_query "
SELECT mac_address, username, nas_ip_address, framed_ip_address,
       acct_session_id, acct_session_time
FROM radius_accounting
WHERE acct_status_type = 'Interim-Update'
FETCH FIRST 20 ROWS ONLY
" | jq '.'
Run static SQL files (d001 projects)
dc-run-sql $(find data -name 'migration-executive-summary.sql') | tee /tmp/exec-summary.json | jq '.'

5. Known Tables
Table Description

radius_authentications

All RADIUS auth events (dot1x, MAB). Columns: TIMESTAMP_TIMEZONE, USERNAME, CALLING_STATION_ID, AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL, PASSED, FAILED, FAILURE_REASON, CREDENTIAL_CHECK, POLICY_SET_NAME, AUTHORIZATION_RULE, FRAMED_IP_ADDRESS

endpoints_data

Endpoint profiler + identity. Columns: MAC_ADDRESS, ENDPOINT_POLICY, IDENTITY_GROUP_ID, STATIC_GROUP_ASSIGNMENT, ENDPOINT_IP, HOSTNAME

endpoint_identity_groups

Group definitions. Columns: ID, NAME, DESCRIPTION, CREATED_BY

network_devices

NAD inventory. Columns: NAME, IP_MASK, PROFILE_NAME, LOCATION, TYPE

radius_accounting

Accounting records. Columns: MAC_ADDRESS, AUDIT_SESSION_ID, ACCT_STATUS_TYPE, ACCT_SESSION_TIME, ACCT_INPUT_OCTETS, ACCT_OUTPUT_OCTETS, ACCT_TERMINATE_CAUSE

endpoints

Legacy endpoint table. Use endpoints_data for JOINs.


6. Environment Differences
Setting d000 (Lab) d001 (Production)

Load credentials

dsource d000 dev/network/ise

dsource d001 dev/network/ise

ISE node

ise-01 (10.50.1.20)

PAN FQDN from dsource

DataConnect DSN

ise-01.inside.domusdigitalis.dev:2484/cpm10

DSN from dsource

Static SQL files

N/A

dc-run-sql $(find data -name 'query.sql')

Inline queries

dc_query "SELECT …​"

dc_query "SELECT …​"

Column names may differ between d000 and d001 ISE versions. Always run the column discovery query (SELECT * FROM table FETCH FIRST 1 ROWS ONLY \| jq '.[0] \| keys') when working in a new environment for the first time.

7. Troubleshooting
xq: Error running jq: ExpatError

ISE returned non-XML (HTML error page). Check HTTP status:

curl -sS -w '\n%{http_code}' --cacert "${ISE_CA_CERT}" -u "${ISE_API_USER}:${ISE_API_PASS}" -H "Accept: application/xml" "https://${ISE_PAN_FQDN}/admin/API/mnt/Session/ActiveList" | tail -1

Common causes: 401 (account disabled/wrong creds), 404 (wrong endpoint), connection refused (ISE down).

other_attr_string returns null

The session is stopped. MnT only populates cert/TLS details during an active 802.1X session. Connect the endpoint, then query again.

dc_query returns empty or errors

Check DataConnect vars are loaded:

printf "DSN: %s\nUSER: %s\n" "$ISE_DATACONNECT_DSN" "$ISE_DATACONNECT_USER"
MnT auth history returns empty

MnT auth history is in-memory only — cleared on ISE restart, limited retention. Use DataConnect radius_authentications for persistent history.

Missing helper functions
source data/shared/scripts/ise-helpers.sh
401 Unauthorized — account disabled

Re-enable the API account in ISE GUI, or verify creds:

printf "FQDN: %s\nUSER: %s\nPORT: %s\nCERT: %s\n" "$ISE_PAN_FQDN" "$ISE_API_USER" "$ISE_ERS_PORT" "$ISE_CA_CERT"
Raw HTTP status check (any MnT endpoint)
mnt_raw=$(curl -sS -w '\n%{http_code}' --cacert "${ISE_CA_CERT}" -u "${ISE_API_USER}:${ISE_API_PASS}" -H "Accept: application/xml" "https://${ISE_PAN_FQDN}/admin/API/mnt/Session/ActiveList")
printf "HTTP: %s\nBody (first 500 chars):\n%.500s\n" "$(tail -1 <<< "$mnt_raw")" "$(sed '$d' <<< "$mnt_raw")"

8. Pipeline Patterns — xq vs jq vs tr
Tool Input When to use

jq

JSON (ERS, DataConnect)

Filtering, reshaping, extracting fields from JSON

xq

XML (MnT)

Convert XML → JSON, then filter with jq-like syntax. xq '.' = full conversion. xq -r '.path' = raw string output.

tr ':!:' '\n'

ISE other_attr_string

ISE packs key=value pairs delimited by :!:. tr splits into one per line for grep.

Full pipeline: MnT XML → xq JSON → tr split → grep filter
mnt "/Session/MACAddress/E0:D5:5D:6C:E1:66" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(Issuer|Subject|TLSCipher)'
MnT XML → xq JSON → jq reshape
mnt "/Session/MACAddress/E0:D5:5D:6C:E1:66" | xq '.sessionParameters | {user_name, framed_ip_address, endpoint_policy}'
DataConnect JSON → jq field extraction
dc_query "SELECT * FROM radius_authentications FETCH FIRST 1 ROWS ONLY" | jq '.[0] | {AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL, CREDENTIAL_CHECK, PASSED}'
ERS JSON → jq nested extraction
ers "/endpoint/${EP_ID}" | jq '.ERSEndPoint | {mac, groupId, customAttributes: .customAttributes.customAttributes}'

9. WLC 9800 RESTCONF API
Prerequisites
Load WLC credentials
dsource d000 dev/network/wlc
Source WLC helper library
source data/d000/infra/wlc-api-reference/scripts/lib/wlc-common.sh

Loaded functions: wlc() (GET), wlc_rpc() (POST), wlc_patch() (PATCH)

Script inventory
find data/d000/infra/wlc-api-reference/scripts/restconf -name '*.sh' -exec basename {} \; | sort
System info
WLC hostname
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-hostname.sh')
WLC full info — model, serial, version
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-wlc-info.sh')
CPU utilization
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-cpu.sh')
Memory usage
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-memory.sh')
Wireless clients and APs
Connected clients
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-clients.sh')
Access points
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-aps.sh')
WLANs and policy
All WLANs
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-wlans.sh')
Policy profiles
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-policy-profiles.sh')
Policy tags
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-policy-tags.sh')
Site tags
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-site-tags.sh')
RADIUS and AAA
RADIUS server config
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-radius.sh')
AAA config
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-aaa.sh')
Network
VLANs
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-vlans.sh')
SVIs
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-svi.sh')
Interfaces
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-interfaces.sh')
RF profiles
RF tags
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-rf-tags.sh')
RF profiles
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-rf-profiles.sh')
RRM config
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'get-rrm.sh')
Full enumeration — all RESTCONF endpoints at once
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'enumerate-all.sh') | tee /tmp/wlc-enumeration-$(date +%F).json
Save WLC config (write memory equivalent)
bash $(find data/d000/infra/wlc-api-reference/scripts/restconf -name 'save-config.sh')
CLI push — automated command delivery (STD-026)
bash $(find data/d000/infra/wlc-api-reference/scripts/cli -name 'wlc-push.sh') 9800-wlc-01 "show version"
Raw RESTCONF calls (using helper function)
GET any YANG path
wlc "/data/Cisco-IOS-XE-wireless-client-oper:client-oper-data/common-oper-data" | jq '.'
POST RPC (save config)
wlc_rpc "/operations/cisco-ia:save-config"

10. Network CLI Tools
net-push — multi-device CLI push
bash $(find data/d000/infra/scripts/cli -name 'net-push.sh') 9800-wlc-01 "show version"
lsof diagnostics
Network connections
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-network.sh')
Serial devices
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-serial.sh')
Files by PID
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-by-pid.sh') <PID>
Open files summary
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-files.sh')
nmcli status
bash $(find data/d000/infra/scripts -name 'nmcli-status.sh')

11. Environment Matrix
Service d000 (Lab) d001 (Production)

ISE credentials

dsource d000 dev/network/ise

dsource d001 dev/network/ise

WLC credentials

dsource d000 dev/network/wlc

dsource d001 dev/network/wlc

ISE helpers

source data/shared/scripts/ise-helpers.sh

source data/shared/scripts/ise-helpers.sh

WLC helpers

source data/d000/infra/wlc-api-reference/scripts/lib/wlc-common.sh

Same lib, different WLC_HOST from dsource

DataConnect

dc_query "SELECT …​" (ISE_DATACONNECT_* from dsource)

dc_query "SELECT …​" or dc-run-sql file.sql

Static SQL files

N/A

dc-run-sql $(find data -name 'query.sql')

Full endpoint profile

MAC="…​" . $(find data -name 'full-endpoint-profile.sh')

Same command, different ISE from dsource

The pattern is always: dsource <tier> <path>source helpers → run scripts. Every script reads from environment variables, never hardcoded. Swap the dsource target and the entire library works against a different environment.

MnT Certificate Details — PMK Cache Investigation

MnT Certificate Details — PMK Cache Investigation

Attempted to retrieve EAP-TLS certificate details from MnT other_attr_string for E0:D5:5D:6C:E1:66 (modestus-p16g wireless, wlan0). Field returns null — session is stale.

Current state
MnT session — stale, stopped
mnt "/Session/MACAddress/E0:D5:5D:6C:E1:66" | xq '.sessionParameters | {user_name, acct_status_type, other_attr_string}'
{
  "user_name": "modestus-p16g.inside.domusdigitalis.dev",
  "acct_status_type": "Stop",
  "other_attr_string": null
}
DataConnect — last ISE-visible auth was June 8 (16 days ago)
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, AUTHENTICATION_METHOD,
       AUTHENTICATION_PROTOCOL, PASSED, FAILURE_REASON
FROM radius_authentications
WHERE CALLING_STATION_ID = 'E0:D5:5D:6C:E1:66'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 1 ROWS ONLY
" | jq '.'
{
  "TIMESTAMP_TIMEZONE": "2026-06-08 16:07:15.527000",
  "USERNAME": "modestus-p16g.inside.domusdigitalis.dev",
  "AUTHENTICATION_METHOD": "dot1x",
  "AUTHENTICATION_PROTOCOL": "EAP-TLS",
  "PASSED": "Pass",
  "FAILURE_REASON": null
}
Root cause — WLC PMK caching

The 9800 WLC caches the Pairwise Master Key (PMK) from the initial EAP-TLS exchange. On every subsequent reconnect, the WLC reuses the cached PMK and never sends a RADIUS request to ISE. This means:

  • MnT never gets a new session → other_attr_string stays null

  • DataConnect radius_authentications has no new records

  • CoA reauth fails because MnT thinks the session is stopped

  • nmcli down/up reconnects via cached PMK — WLC never re-auths with ISE

What was tried
CoA reauth — failed (session already stopped in MnT)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'coa-reauth.sh') E0:D5:5D:6C:E1:66
{
  "remoteCoA": {
    "@requestType": "reauth",
    "results": "false"
  }
}
nmcli reconnect — connected but WLC reused PMK, no RADIUS to ISE
nmcli connection down Domus-WiFi-Mgmt-VLAN100 && sleep 2 && nmcli connection up Domus-WiFi-Mgmt-VLAN100
Clear PMK cache on 9800 WLC
9800-WLC-01# clear wireless client pmk-cache mac-address e0d5.5d6c.e166
Correct syntax is clear wireless client pmk-cache mac-address <mac>. NOT clear wireless pmk-cache client mac-address or clear wireless client mac-address. Cisco MAC format: lowercase dotted (e0d5.5d6c.e166).
Reconnected after PMK clear — still no new auth in ISE
nmcli connection down Domus-WiFi-Mgmt-VLAN100 && sleep 2 && nmcli connection up Domus-WiFi-Mgmt-VLAN100

MnT and DataConnect still show no new authentication after PMK clear + reconnect.

TODO — next session
Verify whether ISE saw a new auth after the PMK clear
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, AUTHENTICATION_METHOD,
       AUTHENTICATION_PROTOCOL, PASSED, FAILURE_REASON
FROM radius_authentications
WHERE CALLING_STATION_ID = 'E0:D5:5D:6C:E1:66'
  AND TIMESTAMP_TIMEZONE > SYSTIMESTAMP - INTERVAL '1' HOUR
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 5 ROWS ONLY
" | jq '.'
Check if the WLAN is configured for 802.1X (SSH to WLC, paste command)
show wlan name Domus-WiFi-Mgmt-VLAN100 | include Security|Authentication
Check WLC client auth state (SSH to WLC, paste command)
show wireless client mac-address e0d5.5d6c.e166 detail | include Policy|Auth|PMK|Session
Check WLC PMK cache for this client (SSH to WLC, paste command)
show wireless client mac-address e0d5.5d6c.e166 detail | include PMK
Clear PMK and verify it’s gone (SSH to WLC, paste commands)
clear wireless client pmk-cache mac-address e0d5.5d6c.e166
show wireless client mac-address e0d5.5d6c.e166 detail | include PMK
Capture WLC output via STD-026 pattern
ssh -tt 9800-wlc-01 2>&1 | tee /tmp/wlc-pmk-investigation-$(date +%F).txt
Once an active EAP-TLS session exists — retrieve cert details
mnt "/Session/MACAddress/E0:D5:5D:6C:E1:66" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n'
Filter cert fields only
mnt "/Session/MACAddress/E0:D5:5D:6C:E1:66" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(Issuer|Subject|Days to Expiry|Serial Number|AKI|Extended Key Usage|Key Usage|TLSCipher|TLSVersion)'
Known MAC addresses
MAC Host Interface Auth Method

E0:D5:5D:6C:E1:66

modestus-p16g

wlan0 (wireless)

EAP-TLS (cert: modestus-p16g.inside.domusdigitalis.dev)

14:F6:D8:7B:31:80

modestus-p16g (or modestus-aw)

wired / wireless

EAP-TLS (cert: p50.inside.domusdigitalis.dev)

A0:59:50:C9:92:EE

unknown — profiled Windows10-Workstation

wireless (capwap)

MAB / iPSK

64:32:A8:C4:C7:19

unknown — profiled Intel-Device

wireless (capwap)

MAB / iPSK

56:38:C0:55:C5:8F

unknown

wireless (capwap)

MAB / iPSK

24:FD:FA:04:A8:33

unknown

wireless (capwap)

MAB / iPSK

Key lesson

MnT other_attr_string with certificate and TLS details is only populated during an active 802.1X session. Once the session stops, the data is gone from MnT. DataConnect radius_authentications is persistent but does not store the cert details — only AUTHENTICATION_PROTOCOL (EAP-TLS) and CREDENTIAL_CHECK (x509_PKI).

To retrieve full cert details (Issuer, Subject, Days to Expiry, TLS cipher), you must query MnT while the session is actively authenticated — not stopped, not cached.

Claude Code Memory System — How It Works

Claude Code Memory System — How It Works

During the 2026-06-24 session, three memories were saved and one updated. This documents the system architecture, what was saved, and why — so you understand the AI tooling you’re using.

Architecture

Claude Code has a file-based persistent memory at:

~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory/

The path is derived from your working directory — each project gets its own memory namespace. Two file types:

File Purpose

MEMORY.md

Index file — one-line pointers to individual memory files. Loaded into every conversation automatically. Max ~200 lines before truncation.

*.md (individual files)

Each memory is a standalone Markdown file with YAML frontmatter (name, description, type). Only loaded when relevant — the index tells Claude which ones to read.

Memory Types
Type What it captures

user

Your role, expertise, preferences — shapes how Claude tailors responses to you specifically

feedback

Corrections and confirmed approaches — "do this" / "don’t do that" with the reason why

project

Ongoing work, discoveries, states — things that aren’t in git or code but matter for context

reference

Pointers to external systems — where to find information outside the repo

What Was Saved This Session
feedback: full-endpoint-profile.sh exists
~/.claude/projects/.../memory/feedback_full-endpoint-profile-exists.md

Trigger: Claude spent an entire session building a duplicate endpoint-profile.sh from scratch, got column names wrong, missed cert/TLS parsing, broke the DataConnect path. The working script was documented in ise-api-scripts.adoc the entire time.

What it prevents: In future sessions, Claude will check data/d001/projects/mschapv2-migration/sql/full-endpoint-profile.sh and ise-api-scripts.adoc before writing any ISE endpoint script.

project: WLC PMK cache blocks MnT cert data
~/.claude/projects/.../memory/project_wlc-pmk-cache.md

Trigger: Investigation revealed that MnT other_attr_string returns null because the 9800 WLC caches PMKs — ISE never sees re-auths. Correct WLC syntax documented: clear wireless client pmk-cache mac-address <mac>.

What it enables: Future sessions start with this context instead of re-diagnosing why MnT cert data is empty.

project: P16g MAC addresses and ISE identity
~/.claude/projects/.../memory/project_p16g-mac-addresses.md

Trigger: Confusion during session about which MAC was which — wired vs wireless, different cert CNs, different ISE policies.

What it prevents: Claude won’t confuse E0:D5:5D:6C:E1:66 (wireless) with 14:F6:D8:7B:31:80 (wired) or suggest the wrong MAC for MnT queries.

feedback: Use proven API calls only (updated)
~/.claude/projects/.../memory/feedback_use-proven-apis.md

Trigger: Claude fabricated raw curl calls for ISE MnT when proven shell scripts (session-report.sh, session-detail.sh) already existed.

What changed: Broadened from just "check DataConnect column names" to "check ALL existing scripts, helpers, and documented workflows before writing anything new."

How Memories Are Used
Phase What happens

Session start

MEMORY.md index is loaded automatically. Claude sees one-line summaries of all memories.

During conversation

When a topic matches a memory description, Claude reads the full file. For example, asking about ISE endpoint profiling triggers reading feedback_full-endpoint-profile-exists.md.

Corrections

When you correct Claude ("don’t do that", "use the existing script"), Claude should save a feedback memory so the correction persists across sessions.

Stale data

Memories are point-in-time. Claude is instructed to verify against current code before acting on memory claims — a memory saying "function X exists at line 42" may be outdated.

Configuration Files
File Purpose

~/.claude/CLAUDE.md

Global behavioral constraints — secrets policy, delegation boundary, CLI coaching, AsciiDoc standards. Applies to ALL projects.

.claude/CLAUDE.md (repo root)

Project-specific instructions — domus-captures standards, file naming, worklog system, project structure, data directory layout.

~/.claude/settings.json

Permissions, hooks, environment variables. Hooks run shell commands on events (e.g., ShellCheck on script edits, AsciiDoc validation).

~/.claude/skills/

Slash commands — /commit, /session, /capture-idea, /project, etc. Each is a prompt template that expands when invoked.

~/.claude/rules/

Path-scoped rules — auto-loaded by file type. asciidoc.md activates when editing .adoc, bash-shell.md for .sh, json.md for .json, yaml.md for .yml.

~/.claude/agents/

Specialized agents — doc-auditor, adoc-linter, build-fixer, project-modularizer. Each has its own tool access and instructions.

Stow Integration

All Claude config is stowed from dots-quantum:

~/atelier/_projects/personal/dots-quantum/claude/.claude/
├── CLAUDE.md
├── settings.json
├── skills/
├── rules/
├── agents/
└── hooks/

stow -t ~ claude symlinks everything into ~/.claude/. Changes to rules, skills, or agents go through dots-quantum → commit → stow.

Memory vs Other Persistence
Mechanism When to use

Memory (~/.claude/projects/…​/memory/)

Facts that matter across conversations — corrections, discovered states, external references

Plans (in-conversation)

Implementation strategy for the current task — alignment before execution

Tasks (in-conversation)

Progress tracking within the current session — discrete steps with status

CLAUDE.md

Durable project/global instructions — standards, invariants, architecture decisions

Daily notes partials

Permanent documentation — investigation results, runbooks, reference material

Memory is the bridge between sessions. Everything else is either ephemeral (plans, tasks) or permanent (CLAUDE.md, partials, git).

Memory File Location — On Disk

Memory files live inside the dots-quantum stow tree, scoped per project:

~/atelier/_projects/personal/dots-quantum/claude/.claude/
  projects/
    -home-evanusmodestus-atelier--bibliotheca-domus-captures/
      memory/
        MEMORY.md                              ← index (auto-loaded)
        feedback_use-proven-apis.md            ← correction memory
        feedback_full-endpoint-profile-exists.md
        project_wlc-pmk-cache.md
        project_p16g-mac-addresses.md
        user_scripting-trajectory.md
        ... (70+ files)

The directory path encodes the working directory — dashes replace path separators. Each repo you work in gets its own memory namespace. After stow -t ~ claude, these symlink into ~/.claude/ where Claude Code reads them.

Find all memories for this project
find ~/atelier/_projects/personal/dots-quantum/claude/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory -name '*.md' | wc -l
Search memory content
grep -rl 'EAP-TLS' ~/atelier/_projects/personal/dots-quantum/claude/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory/
Read a specific memory
cat ~/atelier/_projects/personal/dots-quantum/claude/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory/feedback_use-proven-apis.md
Anatomy of a Memory File

Every memory file has YAML frontmatter and a body. Example — feedback_use-proven-apis.md:

---
name: Use proven API calls only
description: Check existing scripts, helpers, and documented workflows
            before writing raw curl/API calls
type: feedback
---

Before writing any ISE, FMC, BIND, or infrastructure API/CLI command:
1. Check data/d000/infra/ise-api-reference/scripts/ for existing scripts
2. Check data/shared/partials/ for existing proven queries
3. Check recent daily note partials for documented workflows
...

**Why:** Fabricating curl calls when proven scripts exist wastes time...

**How to apply:**
- d001 uses timestamp_timezone not timestamp
- Always use ers(), mnt(), dc_query() helpers — never raw curl
...

The three frontmatter fields drive retrieval:

Field Function

name

Human-readable title — displayed in MEMORY.md index

description

Semantic search key — Claude matches this against conversation context to decide relevance. Specificity matters: "ISE DataConnect column names" triggers on ISE work, "general advice" triggers on nothing.

type

Classification — feedback (behavioral correction), project (ongoing state), user (profile), reference (external pointer)

The body follows a structure for feedback and project types:

  1. The rule or fact — what to do or what is true

  2. Why: — the reason, incident, or constraint that makes this load-bearing

  3. How to apply: — when and where this guidance kicks in

The Why line is critical — without it, the rule becomes a superstition. With it, Claude can judge edge cases: "this rule exists because column names differ between d000 and d001, so it applies when writing DataConnect queries but not when writing ERS calls."

Engineering Implications — AI-Assisted Workflow Design

This memory system is a feedback loop between human expertise and AI context. The engineering patterns:

Correction-driven learning. Every time Claude makes a mistake and you correct it, that correction becomes a feedback memory. The system learns from failure — not from pre-programming. The feedback_use-proven-apis.md memory exists because Claude fabricated curl calls on 2026-06-24 instead of using your tested scripts. That specific failure is now prevented in every future session.

Context window management. The MEMORY.md index is always loaded (~200 lines). Individual memories are loaded on demand by semantic relevance. This is a two-tier retrieval system — index for breadth, full files for depth. The index line for each memory must be specific enough to trigger on the right topics.

Stale data handling. Memories are point-in-time snapshots. Claude is instructed to verify memory claims against current code before acting. A memory saying "function X is at line 42" might be wrong after a refactor. The description field helps — if it says "column names for d001 DataConnect" and the schema changed, the verification step catches it.

Scoping. Memories are project-scoped by directory path. A correction about ISE API patterns in domus-captures doesn’t bleed into domus-docs or domus-infra-ops. This prevents context pollution across repos.

Composability with CLAUDE.md. Memories handle ephemeral corrections and discovered states. CLAUDE.md handles durable invariants and standards. The boundary: if it should apply forever and to all sessions, it belongs in CLAUDE.md. If it was discovered during work and might change, it belongs in memory.

Full Memory Inventory — All Projects

Seven project namespaces exist, each scoped to a working directory:

Project Namespace Memories Index Lines

domus-captures (this repo)

72

70

Principia

7

7

domus-api

3

3

tmux-config

2

2

domus-nvim

1

1

dots-quantum

1

1

tmux-quantum

1

0

87 total memories across all projects. Distribution by type:

Type Count Pattern

feedback

49

Behavioral corrections — the largest category. Each represents a mistake Claude made that you corrected. Accumulates over time.

project

28

Ongoing work states — ISE rotation, PMK cache investigation, CISSP scheduling, Monad pipeline, infrastructure topology.

user

9

Your profile — scripting trajectory, workplace pressure, C/kernel breakthrough, Spanish tutor, documentation ambition.

reference

1

External system pointer — local documentation inventory.

domus-captures — Full Memory Listing (72 files)
Feedback memories (46) — behavioral corrections
feedback_age-encrypt-workflow.md          Must rm stale .age before encrypt-file
feedback_build-adoc-syntax.md            build-adoc file.adoc html --variant name
feedback_build-verify.md                 Never grep build output
feedback_check-before-install.md         command -v before suggesting install
feedback_code-to-files.md               Never dump long code into chat
feedback_commands-copy-ready.md          All sequential commands in one block
feedback_command-substitution-teaching.md User grasped $(id -u) over hardcoded UID
feedback_d000-close-friction.md          Double-encrypt bug + interactive prompt
feedback_d001-assembler-always.md        Every d001 project MUST have assembler
feedback_d001-assembler.md               d001 needs root assembler for PDF gen
feedback_d001-diagrams-always.md         Every ISE project needs .dot topology
feedback_d001-encrypt-before-commit.md   Remind user to d001 close before commit
feedback_d001-workflow.md                d001 is a shell function; partial format
feedback_daily-notes-paradigm.md         Three-layer: shell → index → partials
feedback_dc-run-sql-not-ise-dc-query.md  Static .sql → dc-run-sql; inline → dc_query
feedback_docdir-for-standalone-adoc.md   Standalone .adoc must use {docdir}/images
feedback_dump-to-json.md                 API enumerations go to /tmp files
feedback_elevator-pitch-always.md        Use "Dispositio" not "Elevator Pitch"
feedback_env-never-raw.md               env contains credentials; filter first
feedback_full-endpoint-profile-exists.md Never recreate ISE endpoint scripts
feedback_heredoc-for-python.md           python3 <<'EOF' for inline Python
feedback_jq-not-heredoc.md              jq -n for JSON, not unquoted heredocs
feedback_json-payload-to-file.md         Write API payloads to /tmp files
feedback_latin-rhetoric.md              Use accessible Latin phrases
feedback_monad-transform-patch-full-object.md  PATCH replaces entire object
feedback_monad-v2-output-type.md         v2 uses output_type not type
feedback_network-device-capture.md       STD-026: all CLI/API produce local records
feedback_no-cuentagotas.md              Deliver complete work on first pass
feedback_no-generic-partials.md          Never include shared partials with placeholders
feedback_no-make-build.md               User runs make themselves
feedback_no-prompt-glyph.md             Use $ not ❯ in console blocks
feedback_one-concern-per-block.md        Never mix concerns in one code block
feedback_palaestra-daily-challenge.md    Generate CLI challenge every session
feedback_quijote-diagram-tools.md        Graphviz for character webs, Mermaid for flow
feedback_quijote-light-cyan.md           Use light-cyan variant, not catppuccin
feedback_rhetoric-in-all-communication.md Use classical rhetoric in every response
feedback_spanish-immersion.md            100% Spanish; no English translations
feedback_ssh-push-workflow.md            git push needs user's terminal for ssh-agent
feedback_tee-all-commands.md             tee all API output for audit trail
feedback_test-before-presenting.md       Test commands before presenting
feedback_three-tier-codex.md             pages/partials/examples is intentional
feedback_use-proven-apis.md              Check existing scripts before writing new
feedback_use-user-project-names.md       Never rename projects
feedback_verify-before-acting.md         Read logs/evidence first, hypothesize second
feedback_watermark-diagrams.md           Watermark all diagram outputs
Project memories (19) — ongoing states
project_acme-cert-automation.md          certbot→Vault→ISE automated
project_asciidoc-build-v2.md             New build script with custom roles
project_cissp-scheduling.md              Exam July 12 2026
project_cissp-study-restart.md           Rescheduled to Aug 2026
project_d001-workflow-redesign.md        open/close cycle lost edits
project_home-infra-topology.md           KVM hosts, VMs, VyOS gateway
project_income-streams-assessment.md     19-repo skill surface analysis
project_ise-rotation-2026-06-07.md       ise-01 on kvm-01, phases 0-2 done
project_monad-api-patterns.md            PATCH pipeline, v1 transforms
project_monad-pipeline-lead.md           Monad pipeline leadership
project_p16g-mac-addresses.md            wlan0 vs wired MACs, ISE identities
project_rsyslog-teaching.md              Mauricio wants Linux training
project_session-2026-04-27.md            ISE, VPN, lsof, AppArmor, C
project_session-2026-06-19.md            Hyprlock, WAYLAND-1, tmux env
project_standards-framework.md           26 standards, 5 domains
project_wayland1-phantom-monitor.md      Headless output respawns on P16g
project_william-rsyslog-dispute.md       PM believes rsyslog doesn't store locally
project_wlc-pmk-cache.md                PMK caching prevents MnT cert retrieval
project_yaml-substrate.md               project-master.yaml as machine-readable twin
User memories (7) — your profile
user_c-curriculum-commitment.md          11-phase C curriculum, never truncate
user_c-kernel-breakthrough.md            C→strace→ftrace→AppArmor→ISE connection
user_c-programming-interest.md           C programming trajectory
user_documentation_ambition.md           Enterprise-grade doc output
user_scripting-trajectory.md             2 months in, writing loops from memory
user_spanish-tutor.md                    Pedro (doctor), Quijote edición analítica
user_workplace-pressure.md              CCNP x4 + LPIC-1, assigned automation
Reference memory (1)
reference_local-documentation-system.md  /usr/share/doc, man, info inventory
CLI Commands for Memory Management
List all memory files for this project
find ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory -name '*.md' -not -name 'MEMORY.md' | sort
Count by type
grep '^type:' ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory/*.md | awk -F: '{print $NF}' | sort | uniq -c | sort -rn
Search across all project memories
grep -rl 'search-term' ~/atelier/_projects/personal/dots-quantum/claude/.claude/projects/*/memory/
Read the MEMORY.md index
cat ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/memory/MEMORY.md
Count memories across ALL projects
find ~/atelier/_projects/personal/dots-quantum/claude/.claude/projects/*/memory -name '*.md' -not -name 'MEMORY.md' | wc -l
List all project namespaces with counts
for dir in ~/atelier/_projects/personal/dots-quantum/claude/.claude/projects/*/memory; do
  project=$(basename "$(dirname "$dir")")
  count=$(find "$dir" -name '*.md' -not -name 'MEMORY.md' 2>/dev/null | wc -l)
  printf "%-60s %3d\n" "$project" "$count"
done

Systemd Timer Inventory — Automation Layer Audit

Systemd Timer Inventory

No user crontab — all scheduled automation runs as systemd user timers via ~/.config/systemd/user/. All timer scripts source a shared notification library at ~/.local/bin/domus-notify.

Notification Architecture — domus-notify

Three-tier urgency abstraction over notify-send, Wayland-aware via DBUS_SESSION_BUS_ADDRESS.

Tier Urgency Timeout Sound

Tier 1

low

10s

None

Tier 2

normal

30s

Two short tones (600 Hz → 800 Hz)

Tier 3

critical

Persistent

Three ascending tones (800 → 1000 → 1200 Hz)

Sound generated via speaker-test -t sine — no file dependencies. Audio player detection: pw-playpaplayaplay (first available). Helper functions: days_until "YYYY-MM-DD" (returns negative if past), dow_num (1=Mon, 7=Sun). CLI mode supported: domus-notify critical "Title" "Body".

Timer Schedule — Daily
Time Timer Script Purpose

05:30

cissp-countdown

~/.local/bin/cissp-countdown

CISSP exam countdown (2026-08-16). Morning motivation. Rotates through 8 domains by day of week. Escalation: 21+ days Tier 1, 14-21 Tier 2, 7-14 Tier 2, 3-7 Tier 3, 0-3 Tier 3 nuclear.

06:00

education-pulse

~/.local/bin/education-pulse

Daily education domain rotation. Mon=C curriculum, Tue=Don Quijote, Wed=Math, Thu=Spanish, Fri=CISSP push, Sat=Infrastructure Lab, Sun=Weekly planning. Always Tier 2.

07:15

pacman-news

~/.local/bin/pacman-news

Arch Linux news check. Fetches archlinux.org/news/ via w3m, compares against last pacman -Syu in /var/log/pacman.log. Tier 2 if new items since last upgrade.

08:03

cert-expiry-monitor

~/.local/bin/cert-expiry-monitor

Certificate expiry tracking: ISE EAP-TLS Server Cert, ASA VPN SSL Cert, Vault SSH Signing Cert. Escalation by days remaining: 31+ Tier 1, 14-30 Tier 2, 0-13 Tier 3, expired Tier 3.

08:07 + 14:17

chla-code-alert

scripts/chla-code-alert.sh

PeopleSoft code assignment alerter. Parses AsciiDoc table from partials/trackers/work/peoplesoft-codes/codes.adoc, counts unbillable projects, checks deadline proximity. Weekdays only. --dry-run supported.

20:00

cissp-countdown

~/.local/bin/cissp-countdown

Evening guilt check — same script, second firing.

20:30

dotfiles-sync

~/.local/bin/dotfiles-sync

dots-quantum drift detection. Checks uncommitted changes, unpushed commits, untracked files. Catches stow drift before it compounds. Tier 3 at 5+ issues.

21:07

daily-audit

~/.local/bin/daily-audit

Nightly hygiene sweep across all domus repos + dots-quantum. Four checks: uncommitted changes, unencrypted d001 plaintext on disk, unpushed commits, stale .age files (plaintext newer than encrypted). Tier 3 if plaintext exposed or 5+ issues.

Timer Schedule — Weekly
Schedule Timer Script Purpose

Mon 07:00

security-remediation

~/.local/bin/security-remediation

P16g security assessment progress. Tracks 10 phases (SA-01 through SA-10). Current: SA-01 DONE, SA-04 + SA-09 ACTIVE, rest TODO. Escalation by completion %: <30% Tier 3, 30-70% Tier 2, 70%+ Tier 1.

Mon 07:30

project-stale

~/.local/bin/project-stale

Flags repos with no commits in 14+ days across all domus repos + dots-quantum. Tier 3 if 4+ stale repos.

Sun 19:00

weekly-review

~/.local/bin/weekly-review

Sunday evening accountability. Counts commits across all repos from past 7 days. Includes CISSP countdown. 0 commits = Tier 3 stagnation, 1-4 = Tier 2, 5+ = Tier 1.

System-Level Timers (Arch Defaults)
Schedule Timer Purpose

Daily ~10:45

man-db.timer

Rebuild manpage index

Daily 00:00

shadow.timer

Password expiry housekeeping

Daily ~00:51

plocate-updatedb.timer

Update locate database

Daily ~01:05

systemd-tmpfiles-clean.timer

Clean /tmp per tmpfiles.d rules

Weekly (Mon)

fstrim.timer

SSD TRIM — reclaim freed blocks

Weekly (Tue)

archlinux-keyring-wkd-sync.timer

Sync pacman keyring

Biweekly (Fri)

btrfs-scrub@-.timer

Btrfs filesystem scrub — data integrity check

Non-Timer Systemd User Services
Service Purpose

ssh-agent.service

Persistent SSH agent. Socket at $XDG_RUNTIME_DIR/ssh-agent.socket. Starts on login via WantedBy=default.target.

tmux.service

Detached tmux session on login. ExecStop runs tmux-resurrect save before kill-server. Forking type with 2s restart delay.

Alerts Captured Today (Screenshot)
Alert Severity Details

CHLA Ops — PeopleSoft Codes

ACTION REQUIRED

32 projects without PeopleSoft codes. Hours worked are UNBILLABLE.

Cert Expiry — ISE EAP-TLS Server Cert

CRITICAL

Expired 8 days ago (2026-06-16). Renew via Vault PKI.

Nightly Audit — Uncommitted Work

INFO

220 issues total. domus-captures: 5 files, domus-infra-ops: 3 files uncommitted.

Production Hardening — Applied 2026-06-24

Full audit identified: no concurrent execution guards, inconsistent notification systems, broken audio logic, missing error handlers in 6/10 scripts, hardcoded paths, no notification persistence. All addressed in a 5-phase hardening pass.

Phase 0: Foundation — domus-notify + Central Config
  • Created ~/.config/domus/config.sh (new Stow package domus) — centralized paths, dates, thresholds, repo list

  • Hardened domus-notify:

    • Source guard prevents double-loading

    • Auto-loads config.sh (with fallback defaults)

    • domus_lock()flock-based concurrent execution guard (exit 0 on contention)

    • _domus_require() — dependency validation with tier3 alert on missing commands

    • _domus_log() — persistent notification log at ~/.local/share/domus/notifications.log with auto-rotation at 10k lines

    • notify-send failure detection and journal logging

    • speaker-test availability check + parent-level timeout 2 on audio subshell

Phase 1: Service Unit Hardening (10 files)
Change Rationale

%U specifier replaces hardcoded UID 1000

Portable across users — systemd resolves at runtime

StandardOutput=journal + StandardError=journal

All output queryable via journalctl --user -u domus-*

SyslogIdentifier=domus-%N

Consistent log prefix: domus-cert-expiry-monitor, domus-daily-audit, etc.

Nice=19 + IOSchedulingClass=idle

Background notifications never compete with interactive work

TimeoutStartSec=30 (60 for pacman-news, daily-audit)

Prevents hung scripts from blocking subsequent timer runs

Phase 2: Script Hardening (9 scripts)
  • domus_lock added to every script — flock on /run/user/$(id -u)/domus-<name>.lock

  • trap 'echo "FAILED at line $LINENO: $BASH_COMMAND" >&2' ERR added to 6 scripts missing it

  • Hardcoded paths replaced with config variables: $DOMUS_DIR, $DOTS_DIR, $D001_DIR, $CISSP_EXAM_DATE

  • source domus-notify (bare name — ~/.local/bin is in PATH)

  • pacman-news: _domus_require w3m + timeout 15 on network fetch

  • daily-audit: git upstream detection fixed (distinguishes "no upstream" from "0 ahead"), git errors captured instead of masked

Phase 3: chla-code-alert.sh Consolidation

The only script not using domus-notify — now unified.

  • Removed 5 duplicate functions: notify(), play_sound(), days_until(), today_epoch(), date_to_epoch()

  • Replaced with domus_notify_tier1/2/3 calls with "CHLA Ops" app name

  • Added domus_lock, trap handler

  • CODES_FILE path from config.sh

  • 234 → 170 lines. Sound, logging, locking all unified with the other 9 scripts.

Phase 4: Health Check Meta-Timer (new)

New domus-health script + service + timer (daily 09:00, after morning cluster).

  • Validates all 10 timers: active state, last-run timestamp within expected window, exit code 0

  • Daily timers: 26h window (24h + 2h grace). Weekly timers: 192h (8 days).

  • Fires domus_notify_tier3 if any timer failed or missed its window

  • Persists results to ~/.local/share/domus/health.log

  • Also handles notification log rotation

Deployment Runbook
Step 1 — Dry run (check for Stow conflicts)
cd ~/atelier/_projects/personal/dots-quantum && stow --simulate domus
Step 2 — Deploy all Stow packages + reload systemd
stow domus && stow -R bin && stow -R systemd \
  && systemctl --user daemon-reload \
  && systemctl --user enable --now domus-health.timer
Step 3 — Verify
systemctl --user start domus-health.service \
  && journalctl --user -u domus-health --since "1 minute ago" \
  && tail -5 ~/.local/share/domus/notifications.log
Per-Timer Validation

Trigger each timer manually, check journal output, and verify notification log entry. Run each block independently.

cert-expiry-monitor — certificate expiry tracking
systemctl --user start cert-expiry-monitor.service \
  && journalctl --user -u cert-expiry-monitor --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
chla-code-alert — PeopleSoft code assignment (weekdays only)
systemctl --user start chla-code-alert.service \
  && journalctl --user -u chla-code-alert --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
cissp-countdown — exam countdown (2x daily)
systemctl --user start cissp-countdown.service \
  && journalctl --user -u cissp-countdown --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
daily-audit — uncommitted work, plaintext, unpushed repos
systemctl --user start daily-audit.service \
  && journalctl --user -u daily-audit --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
dotfiles-sync — dots-quantum drift detection
systemctl --user start dotfiles-sync.service \
  && journalctl --user -u dotfiles-sync --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
education-pulse — daily education domain rotation
systemctl --user start education-pulse.service \
  && journalctl --user -u education-pulse --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
pacman-news — Arch news check (requires network)
systemctl --user start pacman-news.service \
  && journalctl --user -u pacman-news --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
project-stale — repos with no commits in 14+ days (Monday)
systemctl --user start project-stale.service \
  && journalctl --user -u project-stale --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
security-remediation — P16g security assessment phases (Monday)
systemctl --user start security-remediation.service \
  && journalctl --user -u security-remediation --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
weekly-review — Sunday evening accountability sweep
systemctl --user start weekly-review.service \
  && journalctl --user -u weekly-review --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
domus-health — meta-timer validation (all timers)
systemctl --user start domus-health.service \
  && journalctl --user -u domus-health --since "1 minute ago" --no-pager \
  && tail -1 ~/.local/share/domus/notifications.log
Fleet status — all timers at a glance
systemctl --user list-timers --all --no-pager
Full notification history
cat ~/.local/share/domus/notifications.log
Reconfigure a timer schedule (edit, reload, verify)
# 1. Edit the timer in dots-quantum
nvim ~/atelier/_projects/personal/dots-quantum/systemd/.config/systemd/user/<name>.timer

# 2. Re-stow and reload
cd ~/atelier/_projects/personal/dots-quantum && stow -R systemd \
  && systemctl --user daemon-reload

# 3. Verify new schedule
systemctl --user show <name>.timer -p TimersCalendar --no-pager
Action Items
  • Renew ISE EAP-TLS Server Cert via Vault PKI — 8 days expired

  • Address PeopleSoft code gap — 32 projects generating unbillable hours

  • Commit outstanding files across domus-captures and domus-infra-ops

  • Review security-remediation SA-04 and SA-09 active phases

  • Deploy hardened timers: stow domus && stow -R bin systemd && systemctl --user daemon-reload

  • Enable health check: systemctl --user enable --now domus-health.timer

MSCHAPv2 to Certificate Migration — New Project Scaffold

New Project: mschapv2-to-cert-migration

Rebuilt the MSCHAPv2 migration project under a better name (mschapv2-to-cert-migration) with full compliance to current standards. Original mschapv2-migration/ preserved untouched for reference.

Why Rebuild
  • Project name reflects the goal (certificate migration), not the problem (MSCHAPv2)

  • Original had ~3,500 lines of inline code in partials — 787 lines removed via code extraction

  • Inline PowerShell (R1-R6, RSAT phases 1-5) and Python (R7-R8, charts) replaced with collapsible include:: references to standalone scripts

  • Shell API toolkit: source data/shared/scripts/ise-helpers.sh loads ers(), mnt(), dc_query() — replaces old copy-paste helper pattern

  • Static SQL via dc-run-sql "$PRJ/sql/file.sql" — replaces dc_query "$(cat {sql-dir}/file.sql)"

  • ifdef::env-d001[] attributes (253 lines) with zeroed fallback — no mutable data in partials

  • $PRJ variable in every command — executable from any directory

  • Cross-platform: Python (Linux/macOS/WSL) + PowerShell (Windows native) + matplotlib charts

  • Progress bars (■■■□□□□□□□ 3/10) instead of emoji status icons

  • Execution checklists as AsciiDoc tables with step numbers, not markdown-style [ ] lists

Standards Applied
Standard Application

STD-001

Partials hold prose + collapsible include:: script references. Code in scripts/ by language. One concern per file.

STD-006

19 files encrypted via d001 close. Nav exclusion for Antora.

STD-020

Graphviz .dot + D2 .d2 diagrams with Catppuccin Mocha. diagrams/README.adoc attribute map.

STD-025

partials/attributes.adoc with ifdef::env-d001[] + zeroed ifndef fallback. All mutable values parameterized (personnel, NADs, policy sets, identity groups, wave counts, metrics).

Code Segmentation

Large inline code blocks extracted to standalone scripts, referenced via [%collapsible] include:: blocks in partials. Partials retain all prose, results, tables, and runbook steps.

Partial Before After What Changed

pwsh-gpo-analytics.adoc

851 lines (66% code)

360 lines (13% code)

R1-R6 PowerShell + R7-R8 Python → 8 collapsible include:: refs

pwsh-rsat-discovery.adoc

533 lines (50% code)

285 lines (30% code)

5 phase scripts → NOTE: refs + captured results preserved

weekly-report.adoc

321 lines

293 lines

Inline Python → include:: to build-enhanced-report.py

dataconnect-report.adoc

494 lines

494 lines

Added Run as Script block with include:: to dc-report-workflow.sh; inline dc_query steps kept (runbook documentation)

mnt-report.adoc

270 lines

270 lines

Added Run as Script block with include:: to mnt-report-workflow.sh

ers-report.adoc

114 lines

114 lines

Added Run as Script block with include:: to ers-inventory.sh

Project Inventory
Layer Count Contents

partials/

18 (3,042 lines)

attributes (253 lines, ifdef::env-d001[]), metadata, summary, team, 5 wave files, gpo-delta-analysis, pwsh-rsat-discovery, pwsh-gpo-analytics (8-report), dataconnect-report, ers-report, mnt-report, endpoint-profile, queries, weekly-report

scripts/python/

6

build-enhanced-report.py (6-sheet Catppuccin Excel), build-full-report.py (raw), device-detail-to-xlsx.py, gpo-delta-graph.py (matplotlib Delta-X/Y chart), gpo-ise-reconciliation.py (R7), gpo-readiness-matrix.py (R8 — 8-sheet Excel)

scripts/powershell/

12

R1-R6 GPO analytics, Get-RsatStatus.ps1, Install-RsatComponents.ps1, Get-8021xGpos.ps1, Export-GpoReports.ps1, Export-GpoInventory.ps1, Get-GpoEapDelta.ps1

scripts/bash/

4

weekly-report-queries.sh, dc-report-workflow.sh, mnt-report-workflow.sh, ers-inventory.sh

sql/

26

All proven DataConnect queries + full-endpoint-profile.sh (3-API deep-dive)

diagrams/

4

migration-architecture.dot + .d2, project-structure.dot, README.adoc (attribute map + Catppuccin palette)

Root

2

mschapv2-to-cert-migration.adoc (assembler), README.adoc (full pipeline)

API Toolkit

All ISE API calls use shell functions from data/shared/scripts/ise-helpers.sh — no netapi Python dependency for API access.

Function API Usage

ers()

ISE ERS (REST, JSON)

ers "/endpoint?size=1" | jq '.SearchResult.total'

mnt()

ISE MnT (XML, pipe through xq)

mnt "/Session/ActiveCount" | xq -r '.sessionCount.count'

dc_query()

ISE DataConnect (Oracle SQL → JSON)

Inline dynamic SQL

dc-run-sql

ISE DataConnect (static .sql files)

dc-run-sql "$PRJ/sql/migration-executive-summary.sql"

Load API toolkit
dsource d001 dev/network/ise
source data/shared/scripts/ise-helpers.sh
GPO Delta Analysis (New)

scripts/powershell/Get-GpoEapDelta.ps1 enumerates WiFi GPO profiles and classifies:

  • Delta-X (cert): EAP-TLS (type 13), TEAP (type 55) — green

  • Delta-Y (non-cert): PEAP/MSCHAPv2 (type 25) — red

Render Delta trend chart
PRJ=~/atelier/_bibliotheca/domus-captures/data/d001/projects/mschapv2-to-cert-migration
uv run --with pandas --with matplotlib python3 "$PRJ/scripts/python/gpo-delta-graph.py"

scripts/python/gpo-delta-graph.py reads delta snapshots over time and produces a Catppuccin Mocha matplotlib trend chart with fill.

Cross-Platform Reporting
Platform Tool Command

Linux/macOS/WSL

Python 3 + uv

uv run --with pandas --with xlsxwriter python3 "$PRJ/scripts/python/build-enhanced-report.py"

Windows (native)

PowerShell + RSAT

12 .ps1 scripts in scripts/powershell/

Chart generation

Python 3 + matplotlib

uv run --with pandas --with matplotlib python3 "$PRJ/scripts/python/gpo-delta-graph.py"

GPO readiness matrix

Python 3 + pandas + xlsxwriter

uv run --with pandas --with xlsxwriter python3 "$PRJ/scripts/python/gpo-readiness-matrix.py"

Render, Build, View

All commands use $PRJ — set once, run from any directory.

Set project root
PRJ=~/atelier/_bibliotheca/domus-captures/data/d001/projects/mschapv2-to-cert-migration
Render Graphviz diagrams
dot -Tsvg "$PRJ/diagrams/migration-architecture.dot" -o "$PRJ/images/migration-architecture.svg"
dot -Tsvg "$PRJ/diagrams/project-structure.dot" -o "$PRJ/images/project-structure.svg"
Render D2 diagram
d2 "$PRJ/diagrams/migration-architecture.d2" "$PRJ/images/migration-architecture-d2.svg"
Build HTML (standalone — not Antora)
build-adoc "$PRJ/mschapv2-to-cert-migration.adoc" html --variant light-cyan -a env-d001
View output
firefox "$PRJ/output/mschapv2-to-cert-migration.html" &
View project structure diagram only
firefox "$PRJ/images/project-structure.svg" &
Commit Summary

19 files encrypted via d001 close. Plaintext guard verified. Pushed with heredoc commit script (/tmp/push-mschapv2-cert.sh).

VNC Blocking — d001 Project Restructure

Project Restructure — PRJ-2026-05-vnc-blocking

Restructured the VNC blocking project from a monolithic appendix-todos pattern into full STD-001 phased archetype with encrypted d001 operational layer.

Problem

The project had all 5 phases inlined as checkboxes inside appendix-todos.adoc — a direct violation of STD-001 ("Phase content MUST live in separate phase-NN-<slug>.adoc partials — never inline in appendix-todos"). No d001 project existed, meaning the 496-line security remediation runbook (Windows/Linux/network discovery and removal commands, evidence collection scripts, nmap sweeps) was sitting in plaintext in a worklog partial at partials/worklog/daily-notes/2026-06/2026-06-09/vnc-removal.adoc.

Standards Applied
Standard Application

STD-001

Phase content extracted into separate partials + container pages. metadata.adoc + summary.adoc required. One concern per file.

STD-006

Nav entries removed per § Nav Exclusion Rule. Pages remain in Antora build (accessible by direct URL) but hidden from sidebar. Sensitive implementation data lives in d001 encrypted.

STD-020

Graphviz .dot and D2 .d2 diagram sources created in diagrams/ with Catppuccin Mocha color palette. diagrams/README.adoc attribute map documents labels, attributes, and hex values.

STD-025

partials/attributes.adoc uses ifdef::env-d001[] blocks with zeroed ifndef fallback. All mutable values ({vnc-port-range-display}, {approved-remote-tools}, {gpo-admin-team}, {siem-platform}, {iot-platform}, etc.) resolve from attributes — never hardcoded.

Antora Layer (public)

Extracted 5 phases from monolithic appendix-todos.adoc into separate partials and container pages:

Partial Content Page Shell

phase-00-discovery.adoc

AQL queries, ISE profiler, FTD/FMC logs, VLAN identification, VNC server version fingerprinting

phase-00.adoc

phase-01-impact-assessment.adoc

Endpoint classification (clinical/research/BMS/admin), department coordination, BMS cross-ref, Claroty visibility

phase-01.adoc

phase-02-alternative-provisioning.adoc

Approved alternatives (RDP+NLA, jump hosts), migration plan, user training

phase-02.adoc

phase-03-blocking-implementation.adoc

FTD ACL rules (TCP 5900-5999, 5800-5899), ISE AuthZ policy, GPO/agent blocking, CR per STD-005

phase-03.adoc

phase-04-validation.adoc

Sentinel/KQL alerting, circumvention monitoring, 7-day validation window, final report

phase-04.adoc

appendix-todos.adoc reduced to administrative items only (carryover tracker, Mandiant cross-reference).

Nav commented out with explanation block:

// VNC Blocking — removed from nav per STD-006 § Nav Exclusion Rule
// Pages remain in Antora build (accessible by direct URL) but hidden from sidebar.
// Sensitive implementation data lives in data/d001/projects/vnc-blocking/ (encrypted).
d001 Layer (encrypted)

Full d001 project directory scaffolded at data/d001/projects/vnc-blocking/.

Render project structure diagram → open in browser
PRJ=~/atelier/_bibliotheca/domus-captures/data/d001/projects/vnc-blocking
dot -Tsvg "$PRJ/diagrams/vnc-project-structure.dot" -o "$PRJ/images/vnc-project-structure.svg" && firefox "$PRJ/images/vnc-project-structure.svg" &
Quick tree view (terminal)
tree ~/atelier/_bibliotheca/domus-captures/data/d001/projects/vnc-blocking
Attribute Implementation (STD-025)

partials/attributes.adoc defines all mutable values with environment gating:

Attribute env-d001 Value Zeroed Fallback

{vnc-port-range-display}

5900-5910

PORT_RANGE_UNDEFINED

{vnc-port-range-http}

5800-5899

PORT_RANGE_UNDEFINED

{vnc-port-range-rfb}

5900-5999

PORT_RANGE_UNDEFINED

{approved-remote-tools}

CyberArk, BeyondTrust, RDP with NLA + MFA

REMOTE_TOOLS_UNDEFINED

{gpo-admin-team}

Windows Team

TEAM_UNDEFINED

{endpoint-team}

Endpoint Engineering

TEAM_UNDEFINED

{siem-platform}

Microsoft Sentinel

SIEM_UNDEFINED

{siem-legacy}

QRadar

SIEM_LEGACY_UNDEFINED

{bms-project-ref}

PRJ-2026-04-bms-device-inventory

PROJECT_REF_UNDEFINED

{iot-platform}

Medigate/Claroty xDome

IOT_PLATFORM_UNDEFINED

Attributes consumed in: summary.adoc (phase status table), windows-removal.adoc (GPO section), network-infra-audit.adoc (ACL audit), post-removal-validation.adoc (validation checklist).

Diagrams (STD-020)

Two diagram sources created with Catppuccin Mocha palette:

vnc-traffic-flow.dot (Graphviz) — Pre-blocking topology showing:

  • Source endpoints by category (clinical, BMS, research, admin) in red/peach/yellow/mauve

  • Network layer (access switch, FTD, ISE) in green/blue

  • Active VNC traffic paths (bold red) vs planned blocking controls (dashed green)

  • Detection flow to QRadar (baseline) and Sentinel (post-blocking)

vnc-blocking-controls.d2 (D2) — 4-layer defense-in-depth architecture:

  • Layer 1: Endpoint controls (GPO/AppLocker/Intune)

  • Layer 2: Network access (ISE AuthZ dACL)

  • Layer 3: Perimeter (FTD ACL DENY TCP 5900-5910)

  • Layer 4: Detection (Sentinel KQL alert → SOC)

  • Each layer enforces independently; bypass cascades to next layer

Render commands (works from any directory via $PRJ):

PRJ=~/atelier/_bibliotheca/domus-captures/data/d001/projects/vnc-blocking
dot -Tsvg "$PRJ/diagrams/vnc-traffic-flow.dot" -o "$PRJ/images/vnc-traffic-flow.svg"
d2 "$PRJ/diagrams/vnc-blocking-controls.d2" "$PRJ/images/vnc-blocking-controls.svg"
Full Pipeline — Render, Build, View

All commands use $PRJ — executable from any directory on any Arch/Linux workstation with dot, d2, build-adoc, and firefox installed.

Full pipeline (render diagrams → build HTML → open browser)
PRJ=~/atelier/_bibliotheca/domus-captures/data/d001/projects/vnc-blocking && \
dot -Tsvg "$PRJ/diagrams/vnc-traffic-flow.dot" -o "$PRJ/images/vnc-traffic-flow.svg" && \
dot -Tsvg "$PRJ/diagrams/vnc-project-structure.dot" -o "$PRJ/images/vnc-project-structure.svg" && \
d2 "$PRJ/diagrams/vnc-blocking-controls.d2" "$PRJ/images/vnc-blocking-controls.svg" && \
build-adoc "$PRJ/vnc-blocking.adoc" html --variant light-cyan -a env-d001 && \
firefox "$PRJ/output/vnc-blocking.html" &

Without -a env-d001 the build produces zeroed fallback values — safe for sharing outside the organization.

Principia Archaeology — grep + find with Regex

Searched the legacy Principia vault (Obsidian/markdown) for VNC prior art using find -regex and grep -rn:

Discovery commands — find files matching VNC across the vault
grep -rl -i 'vnc' /home/evanusmodestus/atelier/_bibliotheca/Principia --include='*.md' | tee /tmp/vnc-principia-files.txt
Content search — VNC + QRadar + JAMF cross-references
grep -rn -iE 'vnc|qradar.*vnc|vnc.*qradar|jamf.*vnc|vnc.*jamf|5900' /home/evanusmodestus/atelier/_bibliotheca/Principia --include='*.md' | tee /tmp/vnc-principia-grep.txt
find -regex variant — POSIX basic, match VNC-related .md files
find /home/evanusmodestus/atelier/_bibliotheca/Principia -regextype egrep -regex '.*(vnc|qradar).*\.md'

Results: 77 files, including a complete January 2026 VNC hunt case.

Treasures Found
Source Content Extracted

OPS-SIEM-QRADAR_LATEX/cases/2026-01-09-vnc-hunt/queries.aql

12 AQL queries — basic through geo lookup, ISE user correlation, internal/external/public classification

OPS-SIEM-QRADAR_LATEX/cases/2026-01-09-vnc-hunt/level-{1..5}-*.py

5 Python analysis scripts at progressive skill levels — CSV to Excel with pivots, summaries, port breakdown

OPS-SIEM-QRADAR_LATEX/cases/2026-01-09-vnc-hunt/VNC-Hunt-Report-2026-01-09.pdf

LaTeX report from the January hunt

03_Captures/2026/01/MEET-2026-01-07-002-claroty-xdome-vnc.md

Claroty xDome meeting: 3 VNC discovery methods (Device Apps, Advanced Filters, Custom Alerts) + JAMF integration

OPS-SIEM-QRADAR_LATEX/reference/queries/AQL-Query-Cookbook.md

AQL cookbook with port-based hunting, flow/event templates, INCIDR patterns

New Partials Created from Principia

4 new partials added to data/d001/projects/vnc-blocking/partials/:

  • qradar-aql-queries.adoc — all 12 AQL queries with context rules and progression table

  • claroty-xdome-discovery.adoc — 3 xDome methods + JAMF integration + alert config

  • macos-discovery-removal.adoc — Screen Sharing, lsof, mdfind, JAMF profile, third-party app removal

  • report-workflow.adoc — CSV export → Python analysis → build-adoc pipeline, Principia reference table

Assembler updated with all 4 new includes.

Pre-Commit Checklist
  • Render diagrams to images/ (dot + d2 commands above)

  • d001 close vnc-blocking — encrypt all partials before commit

  • make — verify Antora build with phase pages (nav-excluded but still compiled)

  • Verify no plaintext .adoc left in data/d001/projects/vnc-blocking/partials/

Endpoint Investigation — d001 Production (5C:FF:35:D8:BB:26)

Endpoint Investigation — d001 Production (5C:FF:35:D8:BB:26)

Full 3-API investigation workflow against production ISE. Same tools built for d000 — swap dsource target.

Prerequisites
Load d001 production credentials
dsource d001 dev/network/ise
Load helper functions
source data/shared/scripts/ise-helpers.sh
Step 1 — Full endpoint profile (canonical tool)

The 3-API combined view — DataConnect 5-table JOIN + ERS identity + MnT session + cert/TLS details.

MAC="5C:FF:35:D8:BB:26" . $(find data -name 'full-endpoint-profile.sh')

Output saved to /tmp/ise-endpoint-profile/:

5CFF35D8BB26-dc.json              ← DataConnect: auth method, protocol, policy, NAD, accounting
5CFF35D8BB26-ers-identity.json    ← ERS: group, custom attributes, static assignment
5CFF35D8BB26-ers-group.json       ← ERS: group name resolution
5CFF35D8BB26-mnt-session.json     ← MnT: live session state
5CFF35D8BB26-mnt-cert-tls.txt     ← MnT: certificate + TLS details (if active 802.1X)
5CFF35D8BB26-mnt-auth-history.json ← MnT: auth history (7 days)
Step 2 — Individual API calls (targeted queries)
ERS — Endpoint identity and custom attributes
Find endpoint by MAC
ers "/endpoint?filter=mac.EQ.5C:FF:35:D8:BB:26" | jq '.SearchResult.resources[0]'
Get full endpoint detail
EP_ID=$(ers "/endpoint?filter=mac.EQ.5C:FF:35:D8:BB:26" | jq -r '.SearchResult.resources[0].id')
ers "/endpoint/${EP_ID}" | jq '.ERSEndPoint | {mac, groupId, description, staticGroupAssignment, customAttributes: .customAttributes.customAttributes}' | tee /tmp/5CFF35D8BB26-ers-detail.json
Resolve group name
GROUP_ID=$(jq -r '.groupId' /tmp/5CFF35D8BB26-ers-detail.json)
ers "/endpointgroup/${GROUP_ID}" | jq '.EndPointGroup | {name, description}'
MnT — Live session
Session detail
mnt "/Session/MACAddress/5C:FF:35:D8:BB:26" | xq '.sessionParameters | {user_name, calling_station_id, framed_ip_address, nas_ip_address, endpoint_policy, acct_status_type, auth_acs_timestamp}'
Certificate + TLS details (active 802.1X sessions only)
mnt "/Session/MACAddress/5C:FF:35:D8:BB:26" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n'
Filter cert fields
mnt "/Session/MACAddress/5C:FF:35:D8:BB:26" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(Issuer|Subject|Days to Expiry|Serial Number|TLSCipher|TLSVersion)'
Filter policy fields
mnt "/Session/MACAddress/5C:FF:35:D8:BB:26" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(ISEPolicySetName|AuthorizationPolicyMatchedRule|IdentityPolicyMatchedRule|AuthenticationStatus|AllowedProtocolMatchedRule)'
Filter profiler/manufacturer fields
mnt "/Session/MACAddress/5C:FF:35:D8:BB:26" | xq -r '.sessionParameters.other_attr_string' | tr ':!:' '\n' | grep -P '^(MFCInfoHardwareManufacturer|MFCInfoHardwareModel|MFCInfoEndpointType|OUI)'
Parse all wire attributes into key-value table
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'parse-attrs.sh') 5C:FF:35:D8:BB:26
Parse as JSON object (pipeable)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'parse-attrs.sh') 5C:FF:35:D8:BB:26 json
MnT — Auth history
Last 7 days (MnT in-memory — cleared on ISE restart)
mnt "/AuthStatus/MACAddress/5C:FF:35:D8:BB:26/604800/20/All" | xq '.. | objects | select(.passed != null) | {passed: .passed."#text", failed: .failed."#text", user_name, authentication_method, authentication_protocol, selected_azn_profiles, acs_timestamp}'
MnT — Session report scripts
Session report — all active sessions
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh')
Session detail — single MAC
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-detail.sh') 5C:FF:35:D8:BB:26
Reshape into nested structure
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'reshape-session.sh') 5C:FF:35:D8:BB:26
CoA reauth (bounce session — forces re-authentication)
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'coa-reauth.sh') 5C:FF:35:D8:BB:26
DataConnect — Persistent auth history
Full auth history for this MAC (last 20 records)
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, CALLING_STATION_ID,
       NAS_IP_ADDRESS, NAS_PORT_ID, DEVICE_NAME,
       AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL,
       PASSED, FAILED, FAILURE_REASON,
       POLICY_SET_NAME, AUTHORIZATION_RULE, AUTHORIZATION_PROFILES,
       IDENTITY_GROUP, CREDENTIAL_CHECK, FRAMED_IP_ADDRESS
FROM radius_authentications
WHERE CALLING_STATION_ID = '5C:FF:35:D8:BB:26'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 20 ROWS ONLY
" | jq '.' | tee /tmp/5CFF35D8BB26-dc-auth.json
5-table JOIN — full context (auth + endpoint + group + NAD + accounting)
dc_query "
SELECT r.calling_station_id AS mac, r.username, r.identity_group,
       r.authentication_method, r.authentication_protocol,
       r.policy_set_name, r.authorization_rule, r.authorization_profiles,
       r.passed, r.failed, r.failure_reason,
       r.nas_ip_address, r.nas_port_id, r.nas_port_type,
       r.framed_ip_address, r.device_name, r.credential_check,
       r.identity_store, r.endpoint_profile,
       e.endpoint_policy AS profiler_policy,
       e.static_group_assignment, e.identity_group_id, e.endpoint_ip, e.hostname,
       g.name AS endpoint_group_name,
       n.name AS nad_name, n.profile_name AS nad_profile,
       a.acct_session_time, a.acct_status_type, a.acct_terminate_cause,
       a.acct_input_octets AS bytes_in, a.acct_output_octets AS bytes_out,
       r.timestamp_timezone
FROM radius_authentications r
LEFT JOIN endpoints_data e ON r.calling_station_id = e.mac_address
LEFT JOIN endpoint_identity_groups g ON e.identity_group_id = g.id
LEFT JOIN network_devices n ON r.nas_ip_address = SUBSTR(n.ip_mask, 1, INSTR(n.ip_mask, '/') - 1)
LEFT JOIN radius_accounting a ON r.audit_session_id = a.audit_session_id AND a.acct_status_type = 'Stop'
WHERE r.calling_station_id = '5C:FF:35:D8:BB:26'
ORDER BY r.timestamp_timezone DESC
FETCH FIRST 1 ROWS ONLY
" | jq '.' | tee /tmp/5CFF35D8BB26-dc-full.json
Endpoint profiler data from endpoints_data table
dc_query "
SELECT *
FROM endpoints_data
WHERE mac_address = '5C:FF:35:D8:BB:26'
FETCH FIRST 1 ROWS ONLY
" | jq '.[0]' | tee /tmp/5CFF35D8BB26-dc-endpoint.json
RADIUS accounting — session time, bytes, terminate cause
dc_query "
SELECT MAC_ADDRESS, USERNAME, NAS_IP_ADDRESS, FRAMED_IP_ADDRESS,
       ACCT_SESSION_ID, ACCT_SESSION_TIME, ACCT_INPUT_OCTETS,
       ACCT_OUTPUT_OCTETS, ACCT_STATUS_TYPE, ACCT_TERMINATE_CAUSE
FROM radius_accounting
WHERE MAC_ADDRESS = '5C:FF:35:D8:BB:26'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 10 ROWS ONLY
" | jq '.' | tee /tmp/5CFF35D8BB26-dc-accounting.json
Auth method breakdown for this MAC
dc_query "
SELECT AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL, CREDENTIAL_CHECK,
       COUNT(*) AS CNT,
       SUM(CASE WHEN FAILED > 0 THEN 1 ELSE 0 END) AS FAILURES
FROM radius_authentications
WHERE CALLING_STATION_ID = '5C:FF:35:D8:BB:26'
GROUP BY AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL, CREDENTIAL_CHECK
ORDER BY CNT DESC
" | jq '.'
Failure analysis — every failed auth with reason
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, AUTHENTICATION_METHOD,
       AUTHENTICATION_PROTOCOL, FAILURE_REASON,
       POLICY_SET_NAME, NAS_IP_ADDRESS, DEVICE_NAME
FROM radius_authentications
WHERE CALLING_STATION_ID = '5C:FF:35:D8:BB:26'
  AND FAILED > 0
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 20 ROWS ONLY
" | jq '.'
Which NADs has this MAC connected to?
dc_query "
SELECT DISTINCT NAS_IP_ADDRESS, DEVICE_NAME, NAS_PORT_ID, NAS_PORT_TYPE
FROM radius_authentications
WHERE CALLING_STATION_ID = '5C:FF:35:D8:BB:26'
ORDER BY NAS_IP_ADDRESS
" | jq '.'
Timeline — when was this MAC first and last seen?
dc_query "
SELECT MIN(TIMESTAMP_TIMEZONE) AS FIRST_SEEN,
       MAX(TIMESTAMP_TIMEZONE) AS LAST_SEEN,
       COUNT(*) AS TOTAL_AUTHS,
       SUM(CASE WHEN FAILED > 0 THEN 1 ELSE 0 END) AS TOTAL_FAILURES
FROM radius_authentications
WHERE CALLING_STATION_ID = '5C:FF:35:D8:BB:26'
" | jq '.[0]'
Step 3 — jq analysis patterns
Group by profile
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'group-by-profile.sh')
Filter — MAB only, dot1x only, missing IP
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'select-filter.sh') mab
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'select-filter.sh') dot1x
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'select-filter.sh') missing-ip
Flag sessions by IP status and auth type
bash $(find data/d000/infra/ise-api-reference/scripts/mnt/jq-patterns -name 'flag-missing-ip.sh')
Step 4 — Update endpoint custom attributes (ERS write)
View current attributes
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') 5C:FF:35:D8:BB:26
Write attributes file
jq -n '{OS:"<OS>", DevicePurpose:"<PURPOSE>"}' > /tmp/ep-attrs.json
Update endpoint — assign group + description + attributes
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') \
  5C:FF:35:D8:BB:26 <GROUP-ID> "<DESCRIPTION>" /tmp/ep-attrs.json
Step 5 — Verify and format results (jq presentation patterns)

After any ERS update, inspect the result and payload with structured output — not raw JSON dumps.

Assign output paths
result="/tmp/endpoint-${MAC//:/}-result.json"
payload="/tmp/endpoint-${MAC//:/}-payload.json"
Verify files exist
ls -l "$result" "$payload"
Styled update report — vertical with null replacement
jq -r '
  def none: "∅";
  "ISE ERS Endpoint Update Result",
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
  (.UpdatedFieldsList.updatedField[] |
    "✓ \(.field)\n  old: \(.oldValue // none)\n  new: \(.newValue // none)\n"
  )
' "$result"

def none: "∅" defines a jq function — reusable within the expression. // is the alternative operator: if .oldValue is null, substitute the symbol. The \n in the string template creates visual separation per field.

Tabular update report — tsv piped to column
{
  printf 'FIELD\tOLD\tNEW\n'
  jq -r '
    .UpdatedFieldsList.updatedField[]
    | [
        .field,
        (.oldValue // "∅"),
        (.newValue // "∅")
      ]
    | @tsv
  ' "$result"
} | column -t -s $'\t'

@tsv is jq’s tab-separated output formatter — takes an array and joins with tabs. The printf header + column -t aligns everything into a clean table. The {…​} compound command groups both streams so column sees headers + data as one input.

Payload inspection — structured field extraction
jq -r '
  .ERSEndPoint as $ep |
  "Endpoint Payload",
  "━━━━━━━━━━━━━━━━",
  "MAC:          \($ep.mac)",
  "Group ID:     \($ep.groupId)",
  "Static group: \($ep.staticGroupAssignment)",
  "Description:  \($ep.description)",
  "OS:           \($ep.customAttributes.customAttributes.OS)",
  "Purpose:      \($ep.customAttributes.customAttributes.DevicePurpose)"
' "$payload"

.ERSEndPoint as $ep binds the nested object to a variable — avoids repeating the path prefix on every line. String interpolation with \(…​) embeds computed values inside literal strings.

jq presentation patterns — quick reference
Pattern What it does

def name: expr;

Define a reusable function within the jq expression

// "fallback"

Alternative operator — substitute when value is null or false

.field as $var

Bind a value to a variable for reuse

@tsv

Format array as tab-separated — pipe to column -t for alignment

@csv

Format array as comma-separated — redirect to .csv file

@base64d

Decode base64 values inline

\(.expr)

String interpolation — embed computed values in literal strings

[.[] | select(…​)]

Filter array elements by condition

group_by(.field) | map({…​})

Aggregate — group then summarize each group

from_entries

Convert [{key, value}] array into an object

to_entries

Convert object into [{key, value}] array — iterate over fields

DataConnect result formatting

The same patterns work on DataConnect output. Example — styled auth timeline:

dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, AUTHENTICATION_METHOD,
       AUTHENTICATION_PROTOCOL, PASSED, NAS_IP_ADDRESS, DEVICE_NAME
FROM radius_authentications
WHERE CALLING_STATION_ID = '${MAC}'
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 10 ROWS ONLY
" | jq -r '
  "Auth Timeline — \(.[0].USERNAME // "unknown")",
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
  (.[] |
    "\(.TIMESTAMP_TIMEZONE)  \(.PASSED)  \(.AUTHENTICATION_PROTOCOL)  \(.DEVICE_NAME)"
  )
'
MnT cert/TLS formatting
mnt "/Session/MACAddress/${MAC}" | xq -r '.sessionParameters.other_attr_string // empty' | tr ':!:' '\n' | awk -F= '
  /^(Issuer|Subject|Days to Expiry|TLSCipher|TLSVersion|Serial Number)/ {
    printf "%-25s %s\n", $1, substr($0, index($0,"=")+1)
  }
'

awk -F= splits on =, printf "%-25s" left-aligns the field name to 25 chars, substr captures everything after the first = (handles values containing = like StepLatency=1=0).

Environment note

All commands work identically in d000 (lab) and d001 (production) — only the dsource target changes:

Environment Command

d000 (lab)

dsource d000 dev/network/ise

d001 (production)

dsource d001 dev/network/ise

The helper functions (ers(), mnt(), dc_query()), the scripts (full-endpoint-profile.sh, session-report.sh, jq patterns), and the DataConnect SQL all read from environment variables set by dsource. No code changes between environments.

find + grep + xargs — Pipeline Composition Patterns

find + grep + xargs — Pipeline Composition Patterns

Commands used during the 2026-06-24 session to locate recently modified files by content, then inspect them. Each pattern builds on the previous — from simple discovery to multi-stage composition.

Pattern 1 — find files modified in the last 60 minutes
Find all .adoc files touched in the last hour
find docs -type f -name '*.adoc' -mmin -60

-mmin -60 means "modification time less than 60 minutes ago." The minus sign means "less than" — -mmin +60 would mean "more than 60 minutes ago." This is `find’s time predicate system:

Predicate Meaning

-mmin -N

Modified less than N minutes ago (recent)

-mmin +N

Modified more than N minutes ago (old)

-mmin N

Modified exactly N minutes ago (rare — use ranges)

-mtime -1

Modified less than 24 hours ago (days, not minutes)

-newer file

Modified more recently than file

-newermt "2026-06-24"

Modified after a specific date (GNU find)

Pattern 2 — find recent files that contain specific patterns
Find .adoc files modified in the last hour that contain find/grep/mtime/mmin
find docs -type f -name '*.adoc' -mmin -60 -exec grep -IlE 'find|grep|mtime|mmin' {} +

Two stages in one command:

  1. find locates files by name and recency

  2. -exec grep -IlE '…​' {} + searches those files for content

The flags on grep:

Flag Purpose

-I

Skip binary files (images, PDFs caught by *.adoc glob won’t match, but safety)

-l

Print only filenames, not matching lines — one path per match

-E

Extended regex — `\pass:[

]` becomes `pass:[

]` for alternation without escaping

The {} + at the end is critical — it batches filenames into a single grep invocation instead of spawning one grep per file. Compare:

Syntax Behavior Performance

-exec grep {} \;

One grep per file found

Slow — N files = N processes

-exec grep {} +

One grep for all files

Fast — one process, multiple args

Pattern 3 — wrap in ls for metadata
Show file details for the matches
ls $(find docs -type f -name '*.adoc' -mmin -60 -exec grep -IlE 'find|grep|mtime|mmin' {} +)

Command substitution $(…​) captures the file list from find | grep and feeds it as arguments to ls. This gives you permissions, size, timestamps — context that find alone doesn’t show.

This pattern breaks on filenames with spaces. For safety with arbitrary paths, use find …​ -print0 | xargs -0 ls -l instead. With .adoc files in your repo (kebab-case, no spaces), $(…​) is safe.
Pattern 4 — find + xargs for directory trees
Find a directory by name, pipe to tree
find -name '2026-06-24' | xargs tree

find locates the directory path, xargs passes it as an argument to tree. This is the find | xargs pipeline — find discovers, xargs acts.

Why xargs instead of -exec: tree isn’t a per-file operation — it needs a directory argument. -exec tree {} \; would work too, but xargs is idiomatic for "feed output of one command as arguments to another."

Pattern 5 — find + xargs with safety flags
Null-delimited for paths with spaces
find docs -type f -name '*.adoc' -mmin -60 -print0 | xargs -0 ls -lh

-print0 outputs null-delimited paths. -0 tells xargs to split on null instead of whitespace. This is the production-safe version of Pattern 3.

Pattern 6 — find + grep + awk composition
Find recent files, search content, extract specific fields
find docs -type f -name '*.adoc' -mmin -60 -exec grep -IlE 'find|grep' {} + | \
  xargs wc -l | sort -rn | head -10

Three stages: find locates → grep filters by content → wc -l | sort ranks by line count. The result: "which recently modified files that mention find/grep are the largest?"

Pattern 7 — find with multiple predicates
Files modified today AND containing a pattern AND larger than 1KB
find docs -type f -name '*.adoc' -newermt "2026-06-24" -size +1k -exec grep -lE 'ISE|WLC|RADIUS' {} +

Predicates are AND by default. Use -o for OR (with parentheses):

Files ending in .sh OR .py modified today
find data -type f \( -name '*.sh' -o -name '*.py' \) -newermt "2026-06-24"
Pattern 8 — process substitution with find
Diff two directory trees without temp files
diff <(find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-23 -name '*.adoc' | sort) \
     <(find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24 -name '*.adoc' | sort)

Process substitution <(…​) feeds each find output as a virtual file to diff. Shows which partials were added, removed, or renamed between days.

Quick reference — the composition toolkit
Tool Role in the pipeline

find

Discovery — locate files by name, type, time, size

grep

Filter — search file contents for patterns

xargs

Bridge — feed discovered paths as arguments to any command

$(…​) (command sub)

Inline — embed output as arguments (ls $(find …​))

<(…​) (process sub)

File — present output as a file descriptor (diff <(find …​) <(find …​))

awk

Transform — extract, format, compute on output

`sort \pass:[

] head`

Rank — order results and take top N

wc -l

Count — measure output volume

tee

Pattern 9 — find + grep + regex alternation
Search across files matching a directory name OR filename pattern
find . -regex '.*\(2026-06-24\|tmux\.adoc\)' -print0 | xargs -0 grep -rn 'simulate'

POSIX basic regex requires escaped \( \| \) for grouping and alternation. The regex matches the path string, not directory contents — so 2026-06-24 matches the directory itself, and grep -rn recurses into it.

The cleaner separation — let find own recursion with -path:

find . \( -path '*2026-06-24/*' -o -name 'tmux.adoc' \) -type f -print0 | xargs -0 grep -n 'simulate'

-path '2026-06-24/' matches files inside the directory (trailing /*), while -name handles exact filenames. -type f ensures only files reach grep.

Pattern 10 — find + grep into nvim (the jump)
Open the matching file at the exact line in nvim
nvim +246 $(find . -path '*2026-06-24/*' -type f -print0 | xargs -0 grep -l 'simulate')

grep -l returns only the filename. +246 is nvim’s line-jump flag. The pipeline reduces to: discover → filter → open.

When you don’t know the line number, let nvim find it:

nvim $(find . -path '*2026-06-24/*' -type f -print0 | xargs -0 grep -l 'simulate') +'/simulate'

+'/pattern' opens the file and jumps to the first match — no line number needed.

Three levels of the same intent:

Command You know Tool handles

nvim +246 file

Exact line

Nothing

nvim file +'/pattern'

The pattern

Finding the line

nvim $(pipeline) +'/pattern'

The query

Finding the file AND line

Why this matters

find discovers. grep filters. xargs bridges. $(…​) inlines. <(…​) virtualizes. Every CLI investigation is a composition of these primitives — the same five tools, different arrangements. The command from today:

ls $(find docs -type f -name '*.adoc' -mmin -60 -exec grep -IlE 'find|grep|mtime|mmin' {} +)

Uses three of the five in one line: find (discovery), grep (filter), $(…​) (inline). Adding | awk or | sort extends without rewriting. That’s the power of composition — each stage is independent, replaceable, and testable alone.

Palaestra: find -regex — POSIX Basic vs Extended

Palaestra: find -regex — Regex Inside find

The previous Palaestra (find + grep + xargs) used -name globs for file discovery and grep -E for content search. This session adds regex inside find itself — matching full paths, not just filenames.

The Problem

-name uses globs (*, ?, []). Globs have no alternation — you cannot express "match A or B" in a single -name pattern:

# This looks for a file literally named '2026-06-24|vnc-blocking.adoc' — fails
find . -name '2026-06-24|vnc-blocking.adoc'

The | is treated as a literal character. Globs don’t have it.

Solution 1 — POSIX Basic Regex (-regex)

find -regex uses POSIX basic regex by default. Grouping () and alternation | must be escaped:

Find today’s daily notes directory AND the VNC blocking assembler
find . -regex '.*\(2026-06-24\|vnc-blocking\.adoc\)'
Output
./data/d001/projects/vnc-blocking/vnc-blocking.adoc
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/vnc-blocking.adoc

Key differences from -name:

Feature -name -regex

Matches against

Filename only (vnc-blocking.adoc)

Full path (./data/d001/projects/vnc-blocking/vnc-blocking.adoc)

Pattern language

Glob (*, ?, [])

POSIX regex (., *, |, \(\), [])

Alternation

Not supported (use -o)

| (basic) or | (extended)

Anchoring

Implicit — matches whole filename

Implicit — matches whole path (must .* prefix)

Solution 2 — Extended Regex (-regextype egrep)

Same result, no escaping — same dialect as grep -E and awk:

find . -regextype egrep -regex '.*(2026-06-24|vnc-blocking\.adoc)'

-regextype egrep changes the regex engine. () and | work bare, like grep -E. This is the form worth drilling — one dialect across grep -E, awk, and now find.

Drill 1 — Find all phase partials across both layers

The VNC project has phase partials in Antora (docs/) and operational partials in d001 (data/). Find everything with "phase" or "vnc" in the path:

POSIX basic
find . -regex '.*\(phase-0[0-4]\|vnc-blocking\).*\.adoc'
Extended
find . -regextype egrep -regex '.*(phase-0[0-4]|vnc-blocking).*\.adoc'
Drill 2 — Find today’s concern partials by topic keyword

Match partials containing "ise", "vnc", "mnt", or "ssh" in the filename — only within today’s daily notes:

find . -regextype egrep -regex '.*/2026-06-24/(ise|vnc|mnt|ssh)[^/]*\.adoc'
Expected output
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/ise-active-session-audit.adoc
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/ise-api-reference-guide.adoc
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/mnt-cert-investigation.adoc
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/ssh-dns-discovery.adoc
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/vnc-blocking.adoc

The [^/]* prevents matching across directory boundaries — anchors the keyword to the filename, not a parent directory.

Drill 3 — Find all d001 project assemblers

Every d001 project has a root assembler: <slug>/<slug>.adoc. Use a backreference to match the repeated slug:

find data/d001/projects -regextype egrep -regex '.*/([^/]+)/\1\.adoc'

This won’t work — find regex doesn’t support backreferences. But it’s worth trying and failing, because knowing the boundary of the tool matters. The working version uses -exec:

find data/d001/projects -maxdepth 2 -name '*.adoc' ! -name 'README.adoc' -not -path '*/partials/*'

Or, use a shell loop for the true "slug matches dirname" test:

for d in data/d001/projects/*/; do
    slug=$(basename "$d")
    [[ -f "${d}${slug}.adoc" ]] && echo "${d}${slug}.adoc"
done
Drill 4 — Find all diagram sources across the project

Match .dot, .d2, or .mmd files anywhere under data/:

find data -regextype egrep -regex '.*\.(dot|d2|mmd)$'
Drill 5 — Combine -regex with other predicates

Find .adoc files modified in the last 2 hours whose path contains "vnc":

find . -regextype egrep -regex '.*vnc.*\.adoc' -mmin -120

Add -exec to inspect:

find . -regextype egrep -regex '.*vnc.*\.adoc' -mmin -120 -exec wc -l {} +
Regex dialect comparison — one table to rule them all
Feature POSIX Basic (find -regex) POSIX Extended (find -regextype egrep, grep -E, awk) PCRE (grep -P)

Alternation

|

|

|

Grouping

\(\)

()

()

One or more

\+ (GNU)

+

+

Optional

\? (GNU)

?

?

Backreference

\1 (limited)

Not guaranteed

\1 (full)

Lookahead

No

No

(?=…​), (?!…​)

Word boundary

No

No

\b

Named group

No

No

(?P<name>…​)

The takeaway: learn POSIX extended first — it’s the dialect shared by grep -E, awk, and find -regextype egrep. When you need lookahead or backreferences, reach for grep -P (PCRE). The basic→extended→PCRE progression is a clean ladder: each level adds, nothing contradicts.

Why this matters beyond sysadmin

The same regex engine shows up everywhere:

  • Pythonre module uses PCRE-like syntax

  • JavaScript — regex literals /pattern/ are PCRE-adjacent

  • SQLREGEXP / SIMILAR TO (PostgreSQL uses POSIX extended)

  • Sentinel KQLmatches regex for log queries

  • IDE search — VS Code regex mode is PCRE

  • API validation — route matching, input sanitization

The thinking is identical across all of these: anchoring, alternation, capture groups, quantifiers. Once the thinking is automatic from daily terminal practice, switching dialects is syntax lookup — not re-learning.

CLI Pattern: sed Append After Pattern Match

Appending a line to a file without opening an editor — sed pattern-addressed a\ command.

The Problem

Insert an include:: directive into an AsciiDoc file after a known anchor line, from the terminal.

sed — Pattern-Addressed Append

# Pattern-addressed — append after the line matching the regex
sed -i '/pattern/a\new line of text' file.adoc

/regex/a\text — when sed matches the regex, it appends text on the next line. -i edits in place (no temp file needed).

Line-addressed variant (brittle — breaks if line numbers shift):

sed -i '19a\new line here' file.adoc

Pattern-addressed variant (survives line drift):

sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc[]' \
  ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc

awk — When You Need More Control

awk '
\{print}
/pattern/{
  print "new line of text"
}' file.adoc > /tmp/out.tmp && mv /tmp/out.tmp file.adoc

Affordance difference: sed -i edits in place natively. awk has no -i — redirect to temp file, then mv back. But awk gives you state machines, multi-line logic, and conditionals that sed’s hold buffer makes painful. For a single-line append, sed is the right tool.

Verify Pattern (Before / Change / After)

# Before
awk 'NR>=17 && NR<=21' file.adoc

# Change
sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc[]' file.adoc

# After
awk 'NR>=17 && NR<=22' file.adoc

Idempotent Version (For Scripts / Future Use)

# Only add if not already present — grep -q guard
grep -q 'tmux-quantum-session-restructure' "$file" || \
  sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc[]' "$file"

grep -q exits silently with 0 (found) or 1 (not found). The || short-circuit means sed only runs when the line is absent. Run it ten times, same result.

Quick Reference

Operation sed awk

Append after pattern

sed -i '/pat/a\text' f

awk '{print} /pat/{print "text"}' f > tmp && mv tmp f

Insert before pattern

sed -i '/pat/i\text' f

awk '/pat/{print "text"} {print}' f > tmp && mv tmp f

Replace line

sed -i '/pat/c\text' f

awk '/pat/{$0="text"} {print}' f > tmp && mv tmp f

Delete line

sed -i '/pat/d' f

awk '!/pat/' f > tmp && mv tmp f

tmux-quantum Session Restructure

Restructured tmux-quantum sessions directory — data-driven audit of all 25 tmuxinator YAMLs against actual git commit frequency and system utilization. Reduced to 18 active sessions (8 new, 3 merged, 16 archived).

tmux-quantum Session Restructure

Context

The sessions/ directory in tmux-quantum had 25 flat YAML files. Half were skeletons with empty panes and placeholder comments. Several pointed to repos with zero recent commits. Four actively-used repos (40+ commits/quarter) had no sessions at all.

Methodology

Audited every repo under ~/atelier/ using git commit frequency (90-day window), file modification timestamps, running tmux sessions, and shell history for tmuxinator start invocations.

Key findings:

Repo 90d Commits Had Session?

domus-captures

1,901

Yes (×2)

dots-quantum

115

Yes

domus-asciidoc-build

40

No

domus-docs

37

No

domus-nvim

25

Yes (×3)

netapi

16

No

domus-infra-ops

14

No

domus-api

13

No

association-engine

8

No

crypta

4

No

13 sessions pointed to repos with zero 90-day commits (Principia paths, obsolete predecessors, inactive projects).

Changes

Created 8 new sessions for repos that lacked them:

  • proj-domus-build — domus-asciidoc-build (converters, themes, templates)

  • proj-netapi — netapi CLI (ISE ERS, DataConnect, reports)

  • proj-domus-infra — domus-infra-ops (infrastructure docs spoke)

  • proj-domus-api — domus-api (FastAPI content management)

  • proj-assoc-engine — association-engine (knowledge graph)

  • proj-domus-docs — Antora aggregator hub

  • proj-domus-spokes — multi-spoke workspace (7 spoke repos in one session)

  • proj-crypta — Rust educational roguelike

Merged 3 → 1:

  • cfg-nvim-lsp + train-nvim + test-nvim-pluginscfg-nvim

  • Three sessions targeting the same repo (domus-nvim) consolidated into one with windows for config, plugins, LSP, telescope, testing

Rewrote 3 sessions:

  • proj-domus-captures — added worklog, drafts, scripts windows matching actual daily workflow; fixed antora window (was opening aggregator playbook instead of component descriptor)

  • daily-fleeting — removed stale npx antora build references, cleaned up

  • cfg-tmux — repointed from generic ~/.config/tmux to tmux-quantum repo with proper module/theme/script windows; fixed tmux source-file ~/.tmux.conf~/.config/tmux/tmux.conf

Fixed 2 sessions:

  • proj-netapi-tui — removed venv/bin/activate references, switched to uv

  • lrn-spanish — removed blocking read -p, added edicion-analitica window for quijote analytical edition

Archived 16 sessions to _archive/:

All zero-activity sessions: cfg-ansible, cfg-email, cfg-oci-vm, proj-tmux-config (obsolete predecessor), proj-aethelred, proj-work (empty dir), data-analysis, lab-vimiff, train-diffview, train-pyats-report-gen, train-python, train-ripgrep, train-sql-py-json, plus the 3 merged originals.

Added INDEX.adoc — quick reference with naming conventions and session descriptions.

Result

sessions/
├── 18 active .yml files     (all YAML-validated)
├── _archive/                (16 archived)
└── INDEX.adoc               (quick reference)

Validation

# Syntax
for f in sessions/*.yml; do yq '.' "$f" > /dev/null 2>&1 && echo "OK $(basename $f)"; done

# Root paths exist
for f in sessions/*.yml; do
  root=$(yq -r '.root' "$f" | sed "s|~|$HOME|")
  [ ! -d "$root" ] && echo "BAD: $(basename $f) → $root"
done

# Name convention (UPPERCASE)
yq -r '.name' sessions/*.yml | awk '$0 != toupper($0) {print "LOWERCASE: " $0}'

Heredoc Commit Scripts — /tmp Workflow

Heredoc Commit Scripts — /tmp Workflow

Multi-step git operations (stage, commit, push) written as bash scripts to /tmp, executed with a one-liner. Safer than pasting complex commands directly — reviewable, re-runnable, composable.

The Pattern

tee writes the script to /tmp while echoing it to stdout — you see exactly what you’re about to run.

tee /tmp/push-myrepo.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

cd ~/path/to/repo

git add file1 file2 dir/

git commit -m "$(cat <<'EOF'
feat(scope): short summary

- Detail one
- Detail two
EOF
)"

git push origin main
SCRIPT

Execute:

bash /tmp/push-myrepo.sh

tee over cat >: you read the script as it’s created. No cat /tmp/push-myrepo.sh needed to verify — the terminal already showed it.

Why This Works
Property Benefit

set -euo pipefail

Stops at first failure — bad stage doesn’t produce an empty commit

Heredoc commit message

Multi-line messages without quoting gymnastics. 'EOF' (quoted) prevents variable expansion inside the message.

Explicit git add

Named files only — never -A or . which can catch secrets or binaries

Script in /tmp

Ephemeral — survives the session but not a reboot. No cleanup needed.

Reviewable

cat /tmp/push-myrepo.sh before executing. Read before you run.

Multi-Repo Push

Chain scripts with && — second repo only pushes if first succeeds:

bash /tmp/push-dots-quantum.sh && bash /tmp/push-domus-captures.sh
Use Cases
1. Standard commit + push (single repo)
tee /tmp/push-infra.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
cd ~/atelier/_bibliotheca/domus-infra-ops
git add docs/modules/ROOT/partials/runbooks/new-runbook.adoc
git commit -m "$(cat <<'EOF'
docs(runbooks): add VyOS BGP peer runbook
EOF
)"
git push origin main
SCRIPT

bash /tmp/push-infra.sh
2. Bulk add with glob pattern
tee /tmp/push-captures.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
cd ~/atelier/_bibliotheca/domus-captures
git add docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/
git add docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24.adoc
git commit -m "$(cat <<'EOF'
docs(worklog): 2026-06-24 daily notes — 12 partials

- ISE API reference, MnT cert investigation, session audit
- Systemd timer inventory + production hardening
- Palaestra: find-grep composition, find-regex, sed-append
EOF
)"
git push origin main
SCRIPT

bash /tmp/push-captures.sh
3. Dots-quantum with Stow validation
tee /tmp/push-dots.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
cd ~/atelier/_projects/personal/dots-quantum

# Validate stow before committing
stow --simulate bin 2>&1 | grep -q "WARNING" || true

git add bin/.local/bin/new-script
git commit -m "$(cat <<'EOF'
feat(bin): add new-script for X

- Description of what it does
- Why it exists
EOF
)"
git push origin main
SCRIPT

bash /tmp/push-dots.sh
4. Encrypted d001 project (close before commit)
tee /tmp/push-d001.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
cd ~/atelier/_bibliotheca/domus-captures

# Verify no plaintext left on disk
plaintext=$(find data/d001 -name '*.adoc' -not -name 'README.adoc' -type f)
if [[ -n "$plaintext" ]]; then
    echo "ABORT: unencrypted d001 files on disk:" >&2
    echo "$plaintext" >&2
    exit 1
fi

git add data/d001/projects/my-project/
git commit -m "$(cat <<'EOF'
docs(d001): update my-project encrypted partials
EOF
)"
git push origin main
SCRIPT

bash /tmp/push-d001.sh
The Nested Heredoc Trick

The commit message uses a heredoc inside a command substitution inside another heredoc. The quoting matters:

git commit -m "$(cat <<'EOF'    (1)
Message content here             (2)
EOF
)"                               (3)
1 'EOF' (single-quoted) — prevents $variable expansion inside the message
2 Write freely — newlines, special characters, backticks all preserved
3 ) closes the command substitution, " closes the -m argument

This is how gach and gacp work under the hood — the same pattern, formalized.

gopass v3 — YAML Format Audit & fd Plumbing

gopass v3 — YAML Format Audit

Context

gopass entries have two YAML formats in the wild:

OLD format — password + --- separator + YAML
hunter2
---
type: ssh-key-passphrase
hostname: ise-01
username: admin

Requires sed '1,/^---$/d' to strip the password and separator before piping to yq.

NEW format — password on line 1, clean YAML from line 2
hunter2
type: ssh-key-passphrase
service: github.com
key_file: ~/.ssh/id_ed25519_github

Works with tail -n +2 | yq . or gopass show entry | yq . directly (gopass treats line 1 as the password, remainder as YAML).

Audit Pipeline

Built an audit pipeline to inventory every entry’s format:

gopass ls --flat | while read entry; do
  gopass show "$entry" 2>/dev/null | awk -v e="$entry" 'NR==2{if($0=="---") print "OLD: " e; else print "NEW: " e}'
done | tee /tmp/gopass-inventory-"$(date +%F)"

Key construct: done | tee — the | pipes the entire loop’s stdout to tee. Without the pipe, tee runs as a separate command reading from the terminal (stdin) — an orphaned process that hangs indefinitely.

Pipe Plumbing and File Descriptors

Every | creates a pair of file descriptors — an fd for writing (stdout of the left side) and an fd for reading (stdin of the right side). The kernel connects them via an anonymous pipe buffer.

The same fd numbers can point to different backing types:

fd Type Example lsof TYPE column

pipe

cmd1 | cmd2

FIFO / pipe

regular file

cmd > output.txt

REG

socket

curl …​;

IPv4 / IPv6

device

cmd > /dev/null

CHR

terminal

interactive shell

CHR (pty)

A multi-stage pipeline wires fd pairs at each joint:

Build and view the fd pipeline diagram
DOC=~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT
dot -Tsvg "$DOC/examples/diagrams/fd-pipeline-plumbing.dot" \
  -o "$DOC/images/diagrams/graphviz/fd-pipeline-plumbing.svg"
firefox "$DOC/images/diagrams/graphviz/fd-pipeline-plumbing.svg"
fd pipeline plumbing — gopass | awk | tee

tee writes to two different fd types simultaneously — the terminal (character device) and the file (regular file). Inspect with:

lsof -p $(pgrep tee) | awk 'NR==1 || /[0-9]/'

The TYPE column reveals what each fd is connected to. This is where lsof shines — not "find the process" (that’s pgrep), but "show me what plumbing this process has."

Process Debugging — Right Tool for the Question
Tool Question It Answers

lsof -p PID

What files/pipes/sockets does this process have open?

pgrep -af pattern

What processes match this name/argument pattern?

ps aux | grep

Same, but shows CPU/MEM/runtime too

pkill -f pattern

Kill by pattern (dangerous — verify with pgrep first)

lsof maps files to processes. pgrep maps patterns to processes. A runaway loop is a process problem, not a file problem — so pgrep is the right tool.

awk -v Variable Binding

The audit pipeline hit a syntax error on first run because the shell variable was passed as a trailing assignment instead of -v:

Broken — trailing assignment
awk 'NR==2{if($0=="---") print "OLD: " ENTRY}' ENTRY="$entry"
Fixed — -v flag binds before execution
awk -v e="$entry" 'NR==2{if($0=="---") print "OLD: " e}'

ENTRY="$entry" after the program text is a file-argument-position assignment — awk evaluates it between files, not before the program runs. -v binds the variable into awk’s namespace before any processing begins.

Inventory Results (2026-06-24)
Category OLD NEW

Credentials (actual secrets)

191

24

.meta entries (structural)

61

95

Total

252

119

68% of all entries still use the old --- separator format.

OLD entries by store prefix
123 v3/domains      ← infrastructure credentials (ISE, switches, servers, PKI)
 65 v3/personal     ← banking, health, housing, subscriptions
 28 v3/professional ← certifications, vendor portals
 18 v2/DOMUS        ← legacy v2 entries (not yet migrated to v3 paths)
  6 v3/keys         ← encryption keys
  6 v2/ARCANA       ← legacy v2 crypto entries
  3 v3/education    ← education portals
  1 v3/licenses
  1 v3/example
  1 v2/PERSONAE

The v2 entries (25 total) need both a path migration and a format migration. v3 entries (227 OLD) need only the --- separator removed.

Migration Strategy
Table 1. Priority tiers
Tier Scope Rationale

1

v3/domains/d000/identity/ (ISE, SSH, AD, FreeIPA)

Most-queried entries — sedyq pipeline simplification

2

v3/domains/d000/servers/ + network/

Infrastructure credentials used in automation scripts

3

v3/personal/ + v3/professional/

Less frequently piped — manual access via gopass show

4

v2/* (25 entries)

Path restructuring + format migration — largest effort per entry

Next Steps
  • Migrate Tier 1 entries (identity/) — manual gopass edit per entry

  • Build batch audit script to verify post-migration (re-run inventory pipeline)

  • Decide on v2 entries: migrate to v3 paths or archive

  • Update gopass-pipeline-patterns.adoc to use tail -n +2 instead of sed '1,/^---$/d'

ise-02 Structural Audit
Show keys with passwords redacted — safe structural audit
gopass show v3/domains/d000/identity/ise/ise-02 | awk '{
    printf "%3d: ", NR
    if(NR==1) {print "[PASSWORD]"; next}
    if(/^---$/) {print "---"; next}
    if(/^[[:space:]]*#/) {print; next}
    sub(/:.*/, ": [REDACTED]")
    print
}' | tee /tmp/ise-02-kv-structure-inventory-"$(date +%F)".yaml | wl-copy
ise-02 entry structure — 69 lines, 3 sections
  1: [PASSWORD]                    <-- gopass primary secret
  2: ---                           <-- OLD format separator (migration target)
  3-14: credentials (nested)       admin/cli/dataconnect — username + password each
 15-16: hostname, ip               device identity
 17-46: notes (multiline)          embedded runbook — query helpers, zsh shortcuts,
                                     password rotation, history commands
 47-59: password_history (array)   2 rotation records — date/expires/method/reason/value
 60-69: patch, persona, ports,     ports: ers/mnt/openapi/pxgrid
        role, type, version        role: secondary, persona: mnt

Migration considerations:

  • Line 2 (---) is the only separator — removing it converts to NEW format

  • The notes: multiline block (lines 17-46) must stay as a YAML multiline string — verify yq parses it correctly after --- removal

  • password_history contains credential values — entries should be migrated with gopass edit, never piped through shell

  • Ports are safe to expose (non-secret): ers, mnt, openapi, pxgrid

ISE Node Migration — OLD → NEW Format

9 ISE nodes, all OLD format:

gopass ls --flat | grep 'identity/ise/' | grep -v '.meta'
v3/domains/d000/identity/ise/ise-02
v3/domains/d001/identity/ise/pmnt
v3/domains/d001/identity/ise/psn-1
v3/domains/d001/identity/ise/psn-2
v3/domains/d001/identity/ise/psn-3
v3/domains/d001/identity/ise/psn-4
v3/domains/d001/identity/ise/smnt
v3/domains/d001/identity/ise/span

All commands use $E. Set once per node, run the workflow.

Pre-flight — redacted structure audit
E=v3/domains/d000/identity/ise/ise-02
gopass show "$E" | awk '{
    printf "%3d: ", NR
    if(NR==1) {print "[PASSWORD] (" length($0) " chars)"; next}
    if(/^---$/) {print "---"; next}
    if(/^[[:space:]]*#/) {print; next}
    sub(/:.*/, ": [REDACTED]")
    print
}'
Edit — three changes
gopass edit "$E"
  1. Delete --- (line 2)

  2. Delete notes: block entirely

  3. Scrub password_history[].value"[ROTATED]"

  4. Add missing key-value pairs per schema below

ise-01 — d000 PAN (reference entry)

Values are literal strings — no variable interpolation. Use actual FQDNs, IPs, versions.

type: "ise"
hostname: "ise-01.inside.domusdigitalis.dev"
ip: "10.50.1.20"
url: "https://ise-01.inside.domusdigitalis.dev:443"
version: "3.5.0.527"
patch: "3"
patch_bundle: "ise-patchbundle-3.5.0.527-Patch3-26040703.SPA.x86_64.tar.gz"
role: "primary"
persona: "pan"
is_pan: "true"
tier: "d000"
deployment: "<standalone|distributed>"
udi:
  spid: "ISE-VM-K9"
  vpid: "V01"
  serial: "4AKH269FK6O"
install_history:
  - date: "2026-06-07 11:31"
    type: "application-install"
    version: "3.5.0.527"
    repository: "SystemDefaultPkgRepos"
  - date: "2026-06-07 13:35"
    type: "patch-install"
    version: "3"
    bundle: "ise-patchbundle-3.5.0.527-Patch3-26040703.SPA.x86_64.tar.gz"
    repository: "nas-01"
credentials:
  admin:
    username: "admin"
    password: "<password>"
  cli:
    username: "admin"
    password: "<password>"
  dataconnect:
    username: "dataconnect"
    password: "<password>"
    port: "2484"
password_policy:
  expiration_days: "45"
  warning_days: "30"
  lockout_retries: "3"
  lockout_timeout_min: "15"
  min_length: "4"
  complexity: "upper+lower+digit"
ports:
  ers: "9060"
  openapi: "443"
  mnt: "443"
  pxgrid: "8910"
  dataconnect: "2484"
  ssh: "22"
conn_limits:
  ers: "30"
  ers_tls: "5"
ssh:
  encryption: "aes128-ctr aes128-gcm aes256-ctr aes256-gcm chacha20-poly1305"
  mac: "hmac-sha1 hmac-sha2-256 hmac-sha2-512"
  host_key: "host-rsa"
  key_algorithm: "rsa-sha2-512 rsa-sha2-256 ssh-rsa"
certs:
  admin: "vault-issued"
  eap-authentication: "vault-issued"
  pxgrid: "vault-issued"
  portal: "vault-issued"
  saml: "vault-issued"
  radius-dtls: "default-self-signed"
network:
  gateway: "10.50.1.1"
  dns:
    - "10.50.1.90"
    - "10.50.1.91"
  ntp: "10.50.1.1"
  ntp_source: "vyos-vip.inside.domusdigitalis.dev"
  ntp_stratum: "3"
  domain: "inside.domusdigitalis.dev"
  timezone: "US/Pacific"
  cdp: "enabled"
  interface: "GigabitEthernet 0"
  netmask: "255.255.255.0"
backup:
  repository: "nas-01"
  url: "nfs://10.50.1.70:/volume1/ise_backups"
password_history:
  - date: "2026-06-24"
    for: "admin-gui+cli"
    value: "[CURRENT]"
    reason: "validated"
    expires: "2026-07-21"
    method: "generated-24char"
  - date: "2026-04-02"
    for: "admin-gui"
    value: "[ROTATED]"
    reason: "expired"
    expires: "2026-07-02"
    method: "generated-24char"
integrations:
  ad:
    type: "external-identity-source"
    hostname: "home-dc01.inside.domusdigitalis.dev"
    join_account: "svc-ise-adjoin"
    password: "<password>"
    base_dn: "DC=inside,DC=domusdigitalis,DC=dev"
    protocol: "ldaps"
    port: "636"
  odbc:
    type: "external-identity-source"
    source_name: "iPSKManager"
    hostname: "ipsk-mgr-01.inside.domusdigitalis.dev"
    port: "3306"
    username: "ipsk-ise"
    password: "<password>"
    database: "ipsk"
nads:
  9300-01:
    type: "switch"
    ip: "<ip>"
    model: "C9300-48U"
    radius_secret: "<shared-secret>"
    tacacs_secret: "<shared-secret>"
    coa_port: "1700"
    location: "home-lab"
    profile: "Cisco"
  9800-wlc-01:
    type: "wlc"
    ip: "<ip>"
    model: "C9800-L"
    radius_secret: "<shared-secret>"
    coa_port: "1700"
    location: "home-lab"
    profile: "Cisco"
  vyos-01:
    type: "router"
    ip: "<ip>"
    model: "VyOS"
    radius_secret: "<shared-secret>"
    location: "home-lab"
    profile: "VyOS"
created: "2026-06-07"
updated: "2026-06-24"
Non-PAN node schema (ise-02 through ise-04, future expansion)
type: "ise"
hostname: "<fqdn>"
ip: "<ip>"
url: "https://<fqdn>:443"
version: "<version>"
patch: "<patch>"
role: "<secondary|standalone>"
persona: "<mnt|psn>"
is_pan: "false"
tier: "d000"
deployment: "<standalone|distributed>"
credentials:
  admin:
    username: "<username>"
    password: "<password>"
  cli:
    username: "<username>"
    password: "<password>"
  dataconnect:
    username: "<username>"
    password: "<password>"
    port: "2484"
ports:
  ers: "9060"
  openapi: "443"
  mnt: "443"
  pxgrid: "8910"
  dataconnect: "2484"
  ssh: "22"
certs:
  admin: "<vault-issued|self-signed|ca-signed>"
  eap-authentication: "<vault-issued|self-signed|ca-signed>"
  pxgrid: "<vault-issued|self-signed|ca-signed>"
  portal: "<vault-issued|self-signed|ca-signed>"
  saml: "<vault-issued|self-signed|ca-signed>"
  radius-dtls: "<vault-issued|self-signed|default-self-signed>"
password_history:
  - date: "<YYYY-MM-DD>"
    for: "<credential-name>"
    value: "[ROTATED]"
    reason: "<reason>"
    expires: "<YYYY-MM-DD>"
    method: "<manual|generated-Nchar>"
created: "<YYYY-MM-DD>"
updated: "<YYYY-MM-DD>"
Verify — structure (no exposure)
gopass show "$E" | tail -n +2 | yq 'keys'
gopass show "$E" | tail -n +2 | yq -r '.url'
gopass show "$E" | tail -n +2 | yq -r '.credentials | keys'
gopass show "$E" | tail -n +2 | yq -r '.ports'
gopass show "$E" | tail -n +2 | yq -r '.certs'
gopass show "$E" | tail -n +2 | yq -r '.password_history[].value'
PAN-only verification
gopass show "$E" | tail -n +2 | yq -r '.integrations | keys'
gopass show "$E" | tail -n +2 | yq -r '.nads | keys'
gopass show "$E" | tail -n +2 | yq -r '.nads | to_entries[] | .key + ": " + .value.type + " @ " + .value.ip'
Test credentials — timed clipboard (45s auto-clear)
# Password length — verify without exposure
gopass show "$E" | tail -n +2 | yq -r '.credentials.admin.password | length'
gopass show "$E" | tail -n +2 | yq -r '.credentials.cli.password | length'
# GUI password → clipboard → paste into https://<hostname>:443
gopass show "$E" | tail -n +2 | yq -r '.credentials.admin.password' | wl-copy
sleep 45 && wl-copy --clear &
# CLI password → clipboard → paste into ssh admin@<ip>
gopass show "$E" | tail -n +2 | yq -r '.credentials.cli.password' | wl-copy
sleep 45 && wl-copy --clear &
Sync and next node
gopass sync
E=v3/domains/d001/identity/ise/span

Career Positioning — LaTeX Build Workflow

Career Positioning — LaTeX Build Workflow

Build the career positioning document from data/d000/cursus/portfolio/.

Quick build + view
cd data/d000/cursus/portfolio && pdflatex -output-directory=/tmp /tmp/career-build.tex 2>&1 | tee /tmp/career-build.log && xdg-open /tmp/career-build.pdf
Step-by-step with tee capture
Build with full log capture
cd data/d000/cursus/portfolio
pdflatex -output-directory=/tmp /tmp/career-build.tex 2>&1 | tee /tmp/career-build.log
Check for errors (grep the log)
grep -E '^\!|^l\.|Error|Warning|Underfull|Overfull' /tmp/career-build.log
Open the PDF
xdg-open /tmp/career-build.pdf
Rebuild after edits
pdflatex -output-directory=/tmp /tmp/career-build.tex 2>&1 | tee /tmp/career-build.log && xdg-open /tmp/career-build.pdf
Files
File Purpose

data/d000/cursus/portfolio/career-positioning-2026-06.tex

Content — sections, descriptions, tables, trajectory

/tmp/career-build.tex

Preamble wrapper — Catppuccin styling, metric boxes, fonts, headers

/tmp/career-build.pdf

Output

/tmp/career-build.log

Build log (captured via tee)

Regenerate the preamble wrapper (if /tmp was cleared)
cat > /tmp/career-build.tex << 'LATEX'
\documentclass[11pt,a4paper]{article}
\usepackage[margin=0.85in]{geometry}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{microtype}
\usepackage[dvipsnames,svgnames,x11names]{xcolor}
\definecolor{accent}{HTML}{89B4FA}
\definecolor{heading}{HTML}{CBA6F7}
\definecolor{subheading}{HTML}{A6E3A1}
\definecolor{bodytext}{HTML}{3C3C4E}
\definecolor{rulecolor}{HTML}{89B4FA}
\definecolor{mocha-subtext}{HTML}{A6ADC8}
\usepackage{enumitem}
\usepackage{booktabs}
\usepackage{tabularx}
\usepackage{hyperref}
\usepackage{titlesec}
\usepackage{fancyhdr}
\usepackage{tikz}
\usepackage{fontawesome5}
\hypersetup{colorlinks=true, linkcolor=accent, urlcolor=accent,
  pdfauthor={Evan Rosado}, pdftitle={Career Positioning — June 2026}}
\titleformat{\section}{\Large\bfseries\color{heading}}{}{0pt}{}
  [\vspace{-4pt}{\color{rulecolor}\rule{\textwidth}{1.5pt}}]
\titleformat{\subsection}{\large\bfseries\color{subheading}}{}{0pt}{}
  [\vspace{-4pt}{\color{rulecolor}\rule{\textwidth}{0.4pt}}]
\titlespacing*{\section}{0pt}{16pt}{8pt}
\titlespacing*{\subsection}{0pt}{12pt}{6pt}
\pagestyle{fancy}\fancyhf{}\renewcommand{\headrulewidth}{0pt}
\fancyfoot[C]{\small\color{mocha-subtext} Evan Rosado $\cdot$ Career Positioning $\cdot$ June 2026 $\cdot$ Page \thepage}
\setlist[description]{style=nextline, leftmargin=1.5cm, labelsep=0pt,
  font=\bfseries\color{accent}, itemsep=6pt}
\setlist[itemize]{leftmargin=1.5cm, itemsep=3pt}
\setlist[enumerate]{leftmargin=1.5cm, itemsep=4pt}
\newcommand{\metricbox}[3]{%
  \begin{tikzpicture}\node[draw=rulecolor, rounded corners=4pt,
    minimum width=3.2cm, minimum height=1.6cm, inner sep=6pt, align=center]{
    {\Large\bfseries\color{accent} #1}\\[2pt]
    {\small\color{bodytext} #2}\\[-1pt]
    {\scriptsize\color{mocha-subtext} #3}};
  \end{tikzpicture}%
}
\begin{document}
\begin{center}
  {\Huge\bfseries\color{heading} Career Positioning}\\[6pt]
  {\large\color{mocha-subtext} Evan Rosado $\cdot$ Information Security Engineer III $\cdot$ June 2026}\\[10pt]
  {\color{rulecolor}\rule{0.6\textwidth}{1.5pt}}
\end{center}\vspace{8pt}
\begin{center}
\metricbox{22}{Repositories}{1,805 commits}\hspace{0.2cm}
\metricbox{144K}{Files}{across all projects}\hspace{0.2cm}
\metricbox{26}{Standards}{documentation system}\hspace{0.2cm}
\metricbox{15+}{Certifications}{CCNP $\times$ 4, LPIC-1}
\end{center}\vspace{12pt}
\input{career-positioning-2026-06.tex}
\end{document}
LATEX

ISE API Capability Comparison — Thomas Howard (Cisco TME) vs Domus Lab

ISE API Capability Comparison — Thomas Howard (Cisco TME) vs Domus Lab

Thomas Howard, Technical Marketing Engineer at Cisco, presented ISE automation patterns on DevNet YouTube (June 2026). This documents the alignment between what Cisco’s own TME demonstrates and what has been independently built in the Domus lab — from terminal zero to production tooling in approximately 2 months.

Source

DevNet YouTube — "Automating Cisco ISE: APIs, SQL, and Ansible in Action" with Thomas Howard (Cisco TME) and Alexi Bisena. Transcript captured to /tmp/thomas-cisco-ise-apis.adoc.

Side-by-Side Comparison
Capability Thomas Howard (Cisco TME) Domus Lab (Evan Rosado)

ERS API access

curl commands, basic GET requests, manual ID lookup for endpoint details

ers() helper function, paginated ers_all(), update-endpoint.sh for writes, group resolution pipeline

Endpoint enumeration

Python script (ice-get) — async IO, pagination, format conversion (JSON/YAML/line)

full-endpoint-profile.sh — 3-API combined (ERS + MnT + DataConnect), 5-table JOIN, cert/TLS parsing, 6 output files per MAC

DataConnect SQL

Basic SELECT queries on radius_accounting, single-table, commented column toggling. "I’m just happy to get basic data out"

5-table JOIN: radius_authentications + endpoints_data + endpoint_identity_groups + network_devices + radius_accounting. Failure analysis, NAD history, timeline, auth method breakdown.

SQL JOINs

"I haven’t played a lot with JOINs"

Production 5-table JOIN with LEFT JOIN, SUBSTR/INSTR for NAD IP extraction from ip_mask, accounting correlation via audit_session_id

MnT API

Not demonstrated

mnt() helper, xq XML→JSON conversion, other_attr_string parsing via tr ':!:', cert/TLS field extraction, auth history, CoA reauth, session report with jq patterns

Output formatting

JSON, YAML, line-delimited JSON

jq styled reports (def none: "∅", @tsv | column -t), awk -F= for MnT cert parsing, printf alignment, Catppuccin-themed Excel via openpyxl

Secrets management

Environment variables in ~/.secrets/ folder, sourced per project

dsource tiered loader (d000/d001), gopass v3 YAML store, age encryption, gocryptfs vaults, LUKS full-disk, 4-layer security model (STD-025)

Python environment

UV for venv, pip for packages

UV-only guard on netapi-tui, /tmp/xlsx-env for disposable work, netapi .venv for production, PEP 668 compliance on Arch

Ansible

Cisco ISE Ansible Galaxy collection, YAML playbooks for trust/endpoint operations

Not yet adopted — shell scripts + jq patterns provide equivalent flexibility with lower overhead. Ansible is a future consideration.

Script distribution

GitHub repository with markdown docs

21-repo Antora hub, 26 standards, CI/CD deployment, encrypted d001 projects, daily note partials as living documentation

Certificate handling

Self-signed cert bypass (--insecure)

Vault PKI CA chain, ISE_CA_CERT variable, proper CA verification. Self-signed only in lab with --cacert

AI integration

"The prompt is the new script" — uses AI to generate scripts from specs

87 persistent memories, project-scoped correction-driven learning, custom skills/rules/hooks/agents, AI collaboration system with version control

Lab infrastructure

Private network via VPN, single ISE node

2 KVM hypervisors, 15+ VMs, VyOS gateway, VRRP, BIND DNS (primary + secondary), Vault cluster, ISE with EAP-TLS cert auth, WLC 9800, k3s cluster, Wazuh SIEM

Key Observations

Thomas Howard is Cisco’s own ISE automation expert — the person who builds the demos, writes the scripts, and presents at DevNet. The comparison is not about who is "better" — it’s about recognizing that the tooling built independently in the Domus lab operates at the same tier as what Cisco’s TME demonstrates publicly.

Three areas where the Domus lab exceeds what was demonstrated:

Area Evidence

DataConnect depth

Thomas does single-table SELECTs. Domus lab does 5-table JOINs with accounting correlation, NAD IP extraction via SUBSTR/INSTR, and failure analysis queries. Thomas explicitly said he hasn’t explored JOINs.

MnT API coverage

Thomas didn’t demonstrate MnT. Domus lab has full coverage: session detail, auth history, cert/TLS extraction from other_attr_string, CoA reauth, active session reporting with 6 jq analysis patterns.

Secrets architecture

Thomas sources env vars from a secrets folder. Domus lab has a 4-layer model: age encryption, gitignore, conditional rendering (ifdef::env-d001[]), pre-commit scrub rules — with tiered access (d000 personal, d001 work, d002 sensitive).

Two areas where Thomas’s approach has advantages:

Area What to learn

Async Python

Thomas’s ice-get uses asyncio for parallel endpoint detail fetches — faster than sequential for loops. Worth adopting in netapi for bulk operations.

Ansible

Declarative YAML playbooks are more shareable with teams than shell scripts. Worth evaluating for d001 production operations where non-engineers need to run procedures.

What This Means for the Portfolio

When a Cisco TME demonstrates ISE automation at a DevNet event, the tooling shown is considered reference-grade — it’s what Cisco wants customers to aspire to. The Domus lab tooling matches or exceeds that reference in 3 of 5 categories, built independently, in 2 months, from terminal zero.

This is not a claim of superiority — Thomas has years of product engineering context. It is evidence that the engineering trajectory is real, the tooling is production-grade, and the capability surface is competitive with vendor-level expertise.

Session Summary — 2026-06-24

Session Summary — 2026-06-24

12 partials. 3 projects. 1 live production change. All three git remotes in sync.

Infrastructure — SSH / DNS / ISE
  • Traced ssh modestus-p50 failure → missing BIND A record → IP 10.50.10.107 discovered via ISE DataConnect 5-table JOIN

  • 4 active wireless MACs audited — all MAB/iPSK, none are Arch workstations

  • P16g MAC identity resolved: E0:D5:5D:6C:E1:66 (wlan0) + A8:2B:DD:8F:23:E6 (wired), both EAP-TLS cert auth

  • Wired endpoint updated: Unknown → Linux-Workstations, OS/DevicePurpose custom attributes populated via ERS API

  • MnT other_attr_string null → root cause: WLC 9800 PMK caching. Correct clear syntax documented.

Abnormal Security — Phase 2 Go-Live (d001 Production)
  • Phase 2 server team expansion executed — Jason added members, ESA bypass deployed

  • Tyler (Abnormal vendor) confirmed platform propagation (20-30 min)

  • Retroactive notification email sent to server team

  • Full project audit: 72 files reviewed, all stale checklists updated, risk register created

  • Tenant Allow/Block List migration partial: variable-chained PowerShell pipeline with @() array forcing, Invoke-Item auto-open

  • export-allow-list.py: openpyxl styled Excel workbook (3 sheets, Catppuccin headers)

  • Phase 3 (all IS) scheduled: Thursday 2026-06-26 09:00

ISE VPX Migration (New Project)
  • ISE secondary ESX VPX repurposing as Synapse VIP landing zone — change window 2026-06-30

  • Full project scaffolded: assembler, partials, Graphviz + D2 diagrams, API verification runbook

  • Email response drafted for Albert Rodriguez

API Tooling
  • ISE API reference: ERS + MnT + DataConnect + WLC RESTCONF + diagnostics — all d000/d001 agnostic

  • jq presentation patterns codified: def none: "∅", @tsv | column -t, .field as $var, awk -F=

  • Endpoint investigation runbook: full 3-API workflow for 5C:FF:35:D8:BB:26 (d001)

  • endpoint-profile.sh + update-endpoint.sh created

Claude Code / AI Engineering
  • Memory system documented: architecture, 87 memories across 7 projects, retrieval mechanics, engineering implications

  • 4 memories saved: full-endpoint-profile.sh canonical tool, WLC PMK cache, P16g MACs, find+grep+nvim pattern

CLI Education (Palaestra)
  • find + grep + xargs pipeline composition — 10 patterns including nvim $(find | grep) jump

  • find -regex — POSIX basic vs extended with 5 drill exercises

  • sed append after pattern match

  • Process substitution + command substitution for DNS zone verification

Tooling
  • git-push-session-partials.sh — parallel push to origin/gitlab/gitea, d001 close integration, --dry-run, color output, failure counting

  • tmux-quantum session restructure — 25 YAMLs audited to 18 active

  • VNC blocking project scaffolded (5 phases)

Tomorrow — Phase 3 Preparation
  • Confirm Phase 2 validation — 24h monitoring clean (Carlos, Alejandro)

  • Confirm full IS member list with William

  • Run Phase 3 pre-change baseline (Step 1 from CR implementation)

  • Execute Phase 3: add all IS to Abnormal-Pilot-Users group (Jason, 09:00)

  • Post-change validation: policy scope, member count, Sentinel telemetry

  • Export Phase 3 final snapshot to JSON

  • Send full expansion validation report to William and Sarah by EOD

  • Endpoint investigation: 5C:FF:35:D8:BB:26 on d001

Arch Linux Vulnerability Assessment & System Hardening

Arch Linux Vulnerability Assessment & Hardening

Trigger

Heard about active Arch Linux security advisories. Concern: if this workstation is compromised, an attacker pivots to SSH keys (16 private keys), age identity (~/.age/identities/personal.key), and GPG keyring — unlocking every encrypted document, git remote, and infrastructure node in the homelab.

Active CVEs Affecting This System
Advisory Package Installed Severity Status

AVG-2901 / CVE-2025-6020

pam

1.7.2-2

High

Vulnerable — no fix released

AVG-2898 / CVE-2025-6170, CVE-2025-49796, CVE-2025-49795, CVE-2025-49794

libxml2

2.15.3-1

High

Still vulnerable at 2.15.3 — denial of service confirmed by arch-audit

linux

current

Medium

Multiple issues, insufficient validation

openssl

current

Medium

Arbitrary command execution, certificate verification bypass

systemd

current

Medium

Information disclosure

coreutils

current

Medium

Information disclosure

perl

current

Medium

Signature forgery, directory traversal

cpio

current

Medium

Arbitrary command execution

wget

current

Medium

Information disclosure

xdg-utils

current

Medium

Information disclosure

Three high-severity: PAM (auth bypass), libxml2 (DoS), linux/linux-lts (multiple + info disclosure). None have upstream fixes yet.

Actionable medium-severity: openssl (command exec + cert bypass) and perl (signature forgery) are both in the trust chain for package verification and TLS — monitor closely.

Verification Commands
# Check if libxml2 is actually patched at 2.15.3
pacman -Qi libxml2 | awk '/^Version/'

# Check Arch security tracker for PAM (AVG-2901)
# Pull the span class (green=fixed, red=vulnerable) and visible text
curl -s 'https://security.archlinux.org/AVG-2901' \
  | grep -oP '<span class="(green|red)">[^<]+' \
  | sed 's/<span class="\([^"]*\)">/[\1] /' | head -5

# List all packages with known vulnerabilities (clean, machine-readable)
# Install first if missing: sudo pacman -S arch-audit
arch-audit
Current Security Posture — What’s Strong
Layer Status

Disk encryption

LUKS on both root (cryptroot, 250G btrfs) and home (crypthome, 1.6T btrfs). Boot partition unencrypted — standard for LUKS. Protects at-rest only.

Firewall

nftables default-drop. Only SSH (port 22) exposed. CUPS, Ollama, other services bound to 127.0.0.1 only.

AppArmor

Active with 60+ profiles (browsers, podman, VS Code, etc.).

Key permissions

All SSH and age keys at 0600. Correct.

Hardware security keys

5 FIDO2/sk-rk variants detected. Strong for services that support them.

Passphrase-protected keys

SSH private keys (d000, d001) are encrypted with passphrases stored in gopass. Exfiltrating the key file alone is insufficient — attacker needs key file + gopass passphrase + gopass master password. Three-layer chain.

SSH modular architecture

config.d/ with numbered priority files (00-exceptions → 80-d000-hosts). Sort-order precedence eliminates the config ordering bug from RCA-2026-03-16-002. Host→IP mappings generated from BIND zone via dig loop, age-encrypted in git, plaintext gitignored. Stow-deployed across machines.

Post-quantum KEX

Client-side ML-KEM-768 + NTRU Prime key exchange active (config built 2026-06-20). Protects against passive interception by quantum-capable adversaries.

Vault SSH certificates

8-hour TTL certs via systemd timer (vault-ssh-cert-renewal.service). Short-lived credentials limit the window for stolen cert reuse. Principals-based signing through Vault SSH mount.

CVE-2026-55200 mitigation

libssh2 vulnerability mitigated via ~/.curlrc protocol block (--proto -sftp -scp). Documented 2026-06-22.

Cisco exception isolation

30-cisco-defaults.conf isolates password auth to network device patterns (9800-wlc-, 3560cx-, etc.). Password auth cannot leak to general hosts — enforced by config.d sort order.

Kernel hardening (partial)

ptrace_scope=1 (restricted), dmesg_restrict=1, unprivileged_bpf_disabled=2.

Encryption layers

Four-layer model (STD-025): LUKS → gitignore → ifdef rendering → scrub rules.

Automated security

security-remediation.service, daily-audit.service, cert-expiry-monitor.service timers active.

Gaps — Pivot Risk Surface
Gap Risk Impact if Exploited

No fail2ban

SSH brute force unmitigated. Port 22 open to all interfaces.

Account compromise → full key access

SSH daemon defaults (server-side)

Client-side config.d/ is hardened (post-quantum KEX, modular architecture). But sshd on this workstation likely runs defaults — PasswordAuthentication enabled, no AllowUsers restriction.

Password spray → shell → full key exfiltration (16 SSH keys + age identity)

kptr_restrict=0

Kernel pointer addresses visible to unprivileged users.

KASLR bypass → kernel exploit chain

rp_filter=0

Reverse path filtering disabled.

IP spoofing on local network

No sysctl.d hardening

No custom kernel security parameters beyond defaults.

Multiple info-leak and network-level attacks

No AppArmor for age/gopass/ssh-agent

These tools run unconfined. Any process running as your user can read ~/.age/identities/personal.key.

User-level compromise → all encrypted data exposed

200+ stale SSH agent sockets

Residual sockets in ~/.ssh/agent/. Unlikely exploitable but poor hygiene.

Theoretical socket hijacking

PAM CVE-2025-6020

Authentication library has known vulnerability, no fix available.

Authentication bypass (details TBD — monitor advisory)

Single age identity

One key decrypts everything. No compartmentalization.

Single point of failure for all encrypted data

Hardening Runbook

All commands below are document-only per STD-007. Copy, review, then execute.

1. SSH Daemon Hardening (Server-Side — sshd)

The client-side SSH config is already hardened — modular config.d/ architecture with post-quantum KEX, Vault-signed 8h certs, and Cisco exception isolation (see 2026-06-20/21 daily notes). What remains unaddressed is the server-side sshd daemon on this workstation, which likely runs defaults.

Create a hardening drop-in to override defaults without touching the main sshd_config:

# Before — inspect current effective config
sudo sshd -T | grep -iE 'permit|password|pubkey|agent|x11|maxauth|grace'
# Create hardening drop-in
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf << 'EOF'
# --- Pivot Prevention ---
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
PubkeyAuthentication yes
AuthenticationMethods publickey

# --- Limit exposure ---
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no

# --- Restrict to your user ---
AllowUsers evanusmodestus

# --- Logging ---
LogLevel VERBOSE
EOF
# Validate config before restarting
sudo sshd -t && echo "Config OK" || echo "CONFIG ERROR — do not restart"

# After — apply and verify
sudo systemctl restart sshd
sudo sshd -T | grep -iE 'permit|password|pubkey|agent|x11|maxauth|grace'
Before disabling PasswordAuthentication, verify your pubkey auth works from another terminal. Do not lock yourself out.
2. Install and Configure fail2ban
sudo pacman -S fail2ban
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 3
backend  = systemd

[sshd]
enabled  = true
port     = ssh
filter   = sshd
maxretry = 3
bantime  = 3600
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
3. Kernel Hardening via sysctl
# Before
sysctl kernel.kptr_restrict kernel.yama.ptrace_scope net.ipv4.conf.all.rp_filter
sudo tee /etc/sysctl.d/99-hardening.conf << 'EOF'
# --- Kernel pointer restriction ---
kernel.kptr_restrict = 2

# --- Restrict ptrace (already 1, escalate to 2 for admin-only) ---
# kernel.yama.ptrace_scope = 2

# --- Network hardening ---
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# --- Prevent SYN flood ---
net.ipv4.tcp_syncookies = 1

# --- Restrict unprivileged user namespaces (container escape mitigation) ---
kernel.unprivileged_userns_clone = 0

# --- Restrict core dumps ---
fs.suid_dumpable = 0
EOF
# Apply without reboot
sudo sysctl --system 2>&1 | grep -E 'error|applying'

# After
sysctl kernel.kptr_restrict net.ipv4.conf.all.rp_filter net.ipv4.conf.all.accept_redirects
If you use rootless Podman/Buildah, unprivileged_userns_clone=0 will break them. Comment that line if needed.
4. AppArmor — Confine age and SSH Agent (Complain-First)

AppArmor profiles can break workflows silently if too restrictive. Strategy: load in complain mode first (logs violations, blocks nothing), run real workflows, review logs, then enforce only after validation.

Step 1: Install apparmor-utils (provides aa-complain, aa-enforce)
sudo pacman -Q apparmor --info | awk '/^Name/'
# aa-complain and aa-enforce should be available — if not:
# sudo pacman -S apparmor-utils
which aa-complain aa-enforce
Step 2: Write the age profile
sudo tee /etc/apparmor.d/usr.bin.age << 'EOF'
#include <tunables/global>

/usr/bin/age {
  #include <abstractions/base>

  # Allow reading recipients and identities
  # Primary key: ~/.secrets/.metadata/keys/master.age.key
  # Secondary: ~/.age/identities/personal.key
  owner @{HOME}/.secrets/.metadata/keys/master.age.key r,
  owner @{HOME}/.secrets/.metadata/keys/master.age.pub r,
  owner @{HOME}/.age/identities/* r,
  owner @{HOME}/.age/recipients/* r,

  # Allow reading files to encrypt/decrypt (broad — tighten per use case)
  owner @{HOME}/** r,

  # Allow writing decrypted output
  owner @{HOME}/** w,
  owner /tmp/** rw,

  # Deny network access — age should never need it
  deny network,

  # Deny access to other users' data
  deny /home/*/ rw,
  deny /root/ rw,
  deny /etc/shadow r,
  deny /etc/passwd w,
}
EOF
Step 3: Load in COMPLAIN mode (logs only — blocks nothing)
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.age
sudo aa-complain /usr/bin/age

# Verify mode
sudo aa-status | grep -A1 'complain'
Step 4: Test real workflows — exercise every age code path
# Basic encrypt/decrypt round-trip (uses personal.key)
echo "test" | age -e -r "$(cat ~/.age/recipients/self.txt)" | age -d -i ~/.age/identities/personal.key
# SSH config decrypt (uses master.age.key — the primary encryption key)
age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.ssh/config.age > /dev/null && echo "GOOD: SSH config decrypt"
# gopass decrypt (uses GPG, but triggers age indirectly via gopass internals)
gopass show v3/domains/d000/identity/ssh/id_ed25519_d000 > /dev/null && echo "GOOD: gopass decrypt"
Step 5: Check what AppArmor would have blocked
sudo dmesg | grep -i 'apparmor.*age' | tail -20

# Or via journalctl
sudo journalctl -k --since "1 hour ago" | grep -i 'apparmor.*age'

If clean (no DENIED entries) — the profile is safe. If violations appear, adjust the profile before enforcing.

Step 6: Enforce ONLY after complain-mode validation
sudo aa-enforce /usr/bin/age
sudo aa-status | grep -A1 'enforce' | grep age
Step 7: Write the ssh-agent profile (same complain-first approach)
sudo tee /etc/apparmor.d/usr.bin.ssh-agent << 'EOF'
#include <tunables/global>

/usr/bin/ssh-agent {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # Socket creation — only in expected locations
  owner @{HOME}/.ssh/agent/ r,
  owner @{HOME}/.ssh/agent/** rw,
  owner /tmp/ssh-*/** rw,
  owner /run/user/[0-9]*/ssh-agent.socket rw,

  # Read keys
  owner @{HOME}/.ssh/id_* r,

  # Deny network (agent doesn't need direct network)
  deny network inet,
  deny network inet6,
}
EOF
# Load in complain mode first
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.ssh-agent
sudo aa-complain /usr/bin/ssh-agent

# Test: load a key, SSH somewhere
ssh-add ~/.ssh/id_ed25519_d000
ssh -o PreferredAuthentications=publickey -i ~/.ssh/id_ed25519_d000 localhost whoami

# Check for violations
sudo dmesg | grep -i 'apparmor.*ssh' | tail -20

# Enforce only after clean validation
# sudo aa-enforce /usr/bin/ssh-agent
4b. AppArmor — Additional Complain-Mode Profiles

These profiles start in complain mode — observe only. Let them soak, check logs, then enforce individually as validated. These are broader binaries, so expect more AUDIT entries before tightening.

AppArmor deny rules are absolute and silent. They enforce even in complain mode and produce no log entries. Complain mode only catches missing allow rules (implicit denials), not explicit deny blocks. To debug deny issues, use strace -e openat — not dmesg or journalctl. See apparmor-curl-cert-fix.adoc (2026-06-25) for the incident where this was learned.
gopass — restricts credential store access and denies raw network
sudo tee /etc/apparmor.d/usr.bin.gopass << 'EOF'
#include <tunables/global>

/usr/bin/gopass {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # GPG integration (gopass calls gpg for decrypt)
  /usr/bin/gpg* px -> /usr/bin/gpg,
  /usr/bin/gpg-agent px -> /usr/bin/gpg-agent,
  owner @{HOME}/.gnupg/** rw,
  owner /run/user/[0-9]*/gnupg/** rw,

  # Gopass store (encrypted entries)
  owner @{HOME}/.local/share/gopass/** rw,
  owner @{HOME}/.config/gopass/** r,

  # Git integration (gopass sync)
  /usr/bin/git px -> /usr/bin/git,

  # Clipboard (gopass show -c)
  /usr/bin/xclip px,
  /usr/bin/xsel px,
  /usr/bin/wl-copy px,
  /usr/bin/wl-paste px,

  # Deny direct network — gopass uses git for sync, not raw sockets
  deny network inet,
  deny network inet6,

  # Deny access outside expected paths
  deny /home/*/ rw,
  deny /root/ rw,
  deny /etc/shadow r,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.gopass
sudo aa-complain /usr/bin/gopass
Validate gopass under complain
gopass show v3/domains/d000/identity/ssh/id_ed25519_d000 > /dev/null && echo "GOOD: gopass decrypt"
sudo dmesg | grep -i 'apparmor.*gopass' | tail -10
curl — allow network but restrict file reads to non-sensitive paths
sudo tee /etc/apparmor.d/usr.bin.curl << 'EOF'
#include <tunables/global>

/usr/bin/curl {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/ssl_certs>

  # Network required — curl's purpose
  network inet stream,
  network inet dgram,
  network inet6 stream,
  network inet6 dgram,

  # DNS resolution
  /etc/resolv.conf r,
  /etc/hosts r,
  /etc/nsswitch.conf r,

  # TLS certificates
  /etc/ssl/** r,
  /etc/ca-certificates/** r,

  # Allow writing to /tmp and current dir for downloads
  owner /tmp/** rw,
  owner @{HOME}/Downloads/** rw,

  # curlrc
  owner @{HOME}/.curlrc r,

  # Deny reading sensitive key material
  deny @{HOME}/.ssh/** r,
  deny @{HOME}/.age/** r,
  # Deny sensitive secrets — but allow CA certs for API tools
  deny @{HOME}/.secrets/.metadata/** r,
  deny @{HOME}/.secrets/gopass/** r,
  deny @{HOME}/.secrets/vaults/** r,
  owner @{HOME}/.secrets/certs/** r,
  deny @{HOME}/.gnupg/private-keys-v1.d/** r,
  deny @{HOME}/.local/share/gopass/** r,

  # Deny access to other users
  deny /home/*/ rw,
  deny /root/ rw,
  deny /etc/shadow r,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.curl
sudo aa-complain /usr/bin/curl
Validate curl under complain
curl -s https://archlinux.org > /dev/null && echo "GOOD: curl network"
sudo dmesg | grep -i 'apparmor.*curl' | tail -10
wget — same pattern as curl
sudo tee /etc/apparmor.d/usr.bin.wget << 'EOF'
#include <tunables/global>

/usr/bin/wget {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/ssl_certs>

  # Network required
  network inet stream,
  network inet dgram,
  network inet6 stream,
  network inet6 dgram,

  # DNS and TLS
  /etc/resolv.conf r,
  /etc/hosts r,
  /etc/nsswitch.conf r,
  /etc/ssl/** r,
  /etc/ca-certificates/** r,

  # Allow writing downloads
  owner /tmp/** rw,
  owner @{HOME}/Downloads/** rw,

  # wgetrc
  owner @{HOME}/.wgetrc r,

  # Deny reading sensitive key material
  deny @{HOME}/.ssh/** r,
  deny @{HOME}/.age/** r,
  # Deny sensitive secrets — but allow CA certs for API tools
  deny @{HOME}/.secrets/.metadata/** r,
  deny @{HOME}/.secrets/gopass/** r,
  deny @{HOME}/.secrets/vaults/** r,
  owner @{HOME}/.secrets/certs/** r,
  deny @{HOME}/.gnupg/private-keys-v1.d/** r,
  deny @{HOME}/.local/share/gopass/** r,

  deny /home/*/ rw,
  deny /root/ rw,
  deny /etc/shadow r,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.wget
sudo aa-complain /usr/bin/wget
python3 — restrict sensitive file reads, allow general execution
sudo tee /etc/apparmor.d/usr.bin.python3 << 'EOF'
#include <tunables/global>

/usr/bin/python3.14 {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/python>
  #include <abstractions/ssl_certs>

  # Python needs broad read for imports
  /usr/lib/python3*/** r,
  /usr/lib/python3*/**.so mr,
  owner @{HOME}/.local/lib/python3*/** r,

  # Network — python scripts need it
  network inet stream,
  network inet dgram,
  network inet6 stream,
  network inet6 dgram,

  # General file access for scripts
  owner @{HOME}/** rw,
  owner /tmp/** rw,

  # Deny reading sensitive key material directly
  deny @{HOME}/.ssh/id_* r,
  deny @{HOME}/.age/identities/** r,
  deny @{HOME}/.secrets/.metadata/keys/** r,
  deny @{HOME}/.gnupg/private-keys-v1.d/** r,

  deny /home/*/ rw,
  deny /root/ rw,
  deny /etc/shadow r,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.python3
sudo aa-complain /usr/bin/python3.14
Validate python under complain
python3 -c "print('GOOD: python runs')"
sudo dmesg | grep -i 'apparmor.*python' | tail -10
git — allow SSH and repo access, deny reading key material directly
sudo tee /etc/apparmor.d/usr.bin.git << 'EOF'
#include <tunables/global>

/usr/bin/git {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/ssl_certs>

  # Network for push/pull/fetch
  network inet stream,
  network inet6 stream,

  # SSH integration (git calls ssh for remotes)
  /usr/bin/ssh px -> /usr/bin/ssh,

  # Git config
  owner @{HOME}/.gitconfig r,
  owner @{HOME}/.config/git/** r,

  # Repo access — broad (git operates on any repo)
  owner @{HOME}/** rw,
  owner /tmp/** rw,

  # Deny reading sensitive key material directly
  deny @{HOME}/.age/identities/** r,
  deny @{HOME}/.secrets/.metadata/keys/** r,
  deny @{HOME}/.gnupg/private-keys-v1.d/** r,
  deny @{HOME}/.local/share/gopass/** r,

  deny /home/*/ rw,
  deny /root/ rw,
  deny /etc/shadow r,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.git
sudo aa-complain /usr/bin/git
Validate git under complain
git -C ~/atelier/_bibliotheca/domus-captures status | head -3
sudo dmesg | grep -i 'apparmor.*git' | tail -10
Load all complain profiles and verify
sudo aa-status | grep -A20 'complain'
Check all logs after a soak period
sudo journalctl -k --since "1 hour ago" | grep -i apparmor | grep -iE 'gopass|curl|wget|python|git'
4c. nftables Egress Filtering

Current firewall only filters inbound (default-drop input). All outbound traffic is unrestricted — an attacker with a shell can exfiltrate to any IP on any port. Add an output chain with allowlisted destinations.

Paste 1 — inspect current rules
sudo nft list ruleset | head -30
Paste 2 — create directory and write egress config
sudo mkdir -p /etc/nftables.d
sudo tee /etc/nftables.d/10-egress.conf << 'EOF'
# Egress filtering — restrict outbound traffic
# Loaded via: include "/etc/nftables.d/*.conf" in nftables.conf

table inet egress {
  chain output {
    type filter hook output priority filter
    policy accept  # Start permissive — switch to drop after validation

    # Always allow loopback
    oif lo accept

    # Allow established/related (responses to inbound)
    ct state {established, related} accept

    # DNS — only to your BIND servers
    ip daddr {10.50.1.90, 10.50.1.91} udp dport 53 accept
    ip daddr {10.50.1.90, 10.50.1.91} tcp dport 53 accept

    # DHCP
    udp dport {67, 68} accept

    # NTP
    udp dport 123 accept

    # SSH outbound (to your infrastructure)
    tcp dport 22 accept

    # HTTPS/HTTP (package updates, git push, web)
    tcp dport {80, 443} accept

    # Vault API
    tcp dport 8200 accept

    # Wazuh agent → manager
    tcp dport 1514 accept
    tcp dport 1515 accept

    # ICMP
    meta l4proto {icmp, icmpv6} accept

    # Log + count anything else (before switching to drop)
    counter log prefix "nft-egress-drop: " group 0
  }
}
EOF
This config starts with policy accept — it logs unmatched outbound traffic but doesn’t block it. Review the logs, then switch to policy drop after validating your workflows still function.
Paste 3 — syntax check (no changes applied — safe)
sudo nft -c -f /etc/nftables.d/10-egress.conf

No output = valid. If errors, fix the file before proceeding.

Paste 4 — inspect nftables.conf before modifying
cat /etc/nftables.conf
Paste 5 — add include (only if not already present)
grep -q 'nftables.d' /etc/nftables.conf || \
  echo 'include "/etc/nftables.d/*.conf"' | sudo tee -a /etc/nftables.conf
Paste 6 — reload and verify
sudo nft -f /etc/nftables.conf
sudo nft list table inet egress
Paste 5 — monitor egress logs (leave running in a tmux pane)
sudo journalctl -kf | grep 'nft-egress-drop'
Switch to drop policy (only after soak period with no unexpected drops)
sudo sed -i 's/policy accept.*permissive/policy drop/' /etc/nftables.d/10-egress.conf
sudo nft -f /etc/nftables.conf
Backout — remove egress chain
sudo nft delete table inet egress
sudo rm /etc/nftables.d/10-egress.conf
5. Audit Key File Access with auditd
sudo pacman -S audit
# Watch all private key files for reads
sudo tee /etc/audit/rules.d/key-access.rules << 'EOF'
-w /home/evanusmodestus/.age/identities/personal.key -p r -k age_key_read
-w /home/evanusmodestus/.ssh/id_ed25519_d000 -p r -k ssh_key_read
-w /home/evanusmodestus/.ssh/id_ed25519_d001 -p r -k ssh_key_read
-w /home/evanusmodestus/.ssh/id_ed25519_github -p r -k ssh_key_read
-w /home/evanusmodestus/.ssh/authorized_keys -p wa -k authorized_keys_mod
-w /home/evanusmodestus/.gnupg/private-keys-v1.d/ -p r -k gpg_key_read
EOF
sudo systemctl enable --now auditd
sudo augenrules --load
# Verify rules loaded
sudo auditctl -l | grep key
# Query who accessed your age key
sudo ausearch -k age_key_read --interpret 2>/dev/null | tail -20
6. SSH Agent Socket Cleanup
# Before — count stale sockets
find ~/.ssh/agent/ -type s -name 'GoFvTTLTra*' 2>/dev/null | wc -l
# Remove stale agent sockets (not current session)
find ~/.ssh/agent/ -type s -name 'GoFvTTLTra*' -mtime +1 -delete 2>/dev/null

# After
find ~/.ssh/agent/ -type s 2>/dev/null | wc -l
7. Restrict Age Identity — Compartmentalization

The single personal.key decrypts everything. Consider splitting into purpose-scoped identities:

# Generate separate identities
age-keygen -o ~/.age/identities/infra.key 2>&1 | tee ~/.age/recipients/infra.pub
age-keygen -o ~/.age/identities/personal-legal.key 2>&1 | tee ~/.age/recipients/personal-legal.pub
age-keygen -o ~/.age/identities/work.key 2>&1 | tee ~/.age/recipients/work.pub

# Lock permissions
chmod 600 ~/.age/identities/*.key
chmod 644 ~/.age/recipients/*.pub

Then re-encrypt sensitive files with the appropriate key. Infrastructure secrets use infra.key, d000 personal uses personal-legal.key, d001 work uses work.key. A compromise of one key doesn’t unlock the others.

8. Wazuh Agent — Enroll This Workstation

Wazuh 4.14.3 runs on k3s (k3s-master-01, single-node) with manager at 10.50.1.132. All 4 pods healthy (dashboard, indexer, manager-master, manager-worker). No agents enrolled yetagent_control -l shows only the manager itself (ID 000). The 7 server agents from the project plan and 3 workstation agents are all pending. pfSense syslog active via UDP 514.

Enrolling this workstation closes the visibility gap: every hardening action above (sshd, fail2ban, AppArmor, auditd) becomes visible and alertable in the Wazuh dashboard.

What Wazuh agent provides on this workstation
  • File integrity monitoring (FIM) — realtime alerts if ~/.ssh/, ~/.age/identities/, ~/.gnupg/private-keys-v1.d/ are modified

  • Log analysis — sshd auth failures, fail2ban bans, AppArmor denials forwarded to OpenSearch indexer

  • Vulnerability detection — Wazuh’s CVE scanner supplements arch-audit

  • Rootkit detectionrootcheck module scans for known rootkit signatures

  • Active response — Wazuh can trigger its own IP bans (complements fail2ban)

Known blockers (from project status)
  • k3s NAT rule 170 (10.42.0.0/16) — blocking indexer pod communication, deferred to Q3

  • Archives indexing — logs reach /var/ossec/logs/archives/archives.log but OpenSearch indices not created (<logall>yes</logall> and <logall_json>yes</logall_json> need verification)

  • Agent enrollment should still work — the agent→manager path is separate from the indexer issue

Step 1: Install Wazuh agent on Arch
# Arch is not officially supported — AUR or source build
# Check AUR first
yay -Ss wazuh-agent

# If unavailable, follow the generic Linux tarball install from Wazuh docs
# or build from source (see codex/security/wazuh.adoc for patterns)
Step 2: Configure agent to point to k3s Wazuh manager
# Before
sudo awk '/<address>/' /var/ossec/etc/ossec.conf

# Change — set manager IP (k3s service endpoint)
sudo sed -i 's|<address>MANAGER_IP</address>|<address>10.50.1.132</address>|' /var/ossec/etc/ossec.conf

# After
sudo awk '/<address>/' /var/ossec/etc/ossec.conf
Step 3: Add FIM for sensitive key directories
# Add syscheck directives — realtime monitoring on key material
sudo tee -a /var/ossec/etc/ossec.conf.d/fim-keys.conf << 'EOF'
<ossec_config>
  <syscheck>
    <directories check_all="yes" realtime="yes">/home/evanusmodestus/.ssh</directories>
    <directories check_all="yes" realtime="yes">/home/evanusmodestus/.age/identities</directories>
    <directories check_all="yes" realtime="yes">/home/evanusmodestus/.gnupg/private-keys-v1.d</directories>
    <directories check_all="yes" realtime="yes">/etc/ssh/sshd_config.d</directories>
  </syscheck>
</ossec_config>
EOF
Step 4: Enable and start
sudo systemctl enable --now wazuh-agent
sudo systemctl status wazuh-agent
Step 5: Verify enrollment from manager
# From Wazuh manager pod or via API
ssh wazuh '/var/ossec/bin/agent_control -l' | grep modestus

# Or via Wazuh API
curl -k -u <user>:<pass> "https://10.50.1.132:55000/agents?pretty&q=name~modestus"
This enrollment is part of the existing Wazuh SIEM project (PRJ, 105 days open, P0). The workstation checklist item is already tracked — completing it here advances that project. See Wazuh SIEM Portfolio and the CR-2026-02-26 change request for the full rollout plan.
9. Install arch-audit for Ongoing Monitoring
sudo pacman -S arch-audit

# Run immediately
arch-audit

# Add to daily-audit systemd timer
# (check if your existing daily-audit.service already calls this)
systemctl --user cat daily-audit.service
Packages to Evaluate for Removal

Review whether these are needed — each is attack surface:

# List explicitly installed packages (not dependencies)
pacman -Qe | wc -l

# Check for known-vulnerable installed packages
arch-audit 2>/dev/null

# Packages that increase attack surface if unused:
# - cups (printing — listening on 631, even if localhost)
# - avahi (mDNS — network discovery)
# - any X11 forwarding libs if X11Forwarding is disabled
pacman -Q cups avahi 2>/dev/null
Priority Order
Priority Action Effort

P0

SSH hardening drop-in — /etc/ssh/sshd_config.d/10-hardening.conf applied. PasswordAuth off, AllowUsers evanusmodestus, MaxAuthTries 3, agent forwarding off.

■■■■■■■■■■ 10/10

P0

fail2ban — /etc/fail2ban/jail.local configured. sshd jail active, 3 retries, 1h ban, systemd backend.

■■■■■■■■■■ 10/10

P1

sysctl.d kernel hardening — /etc/sysctl.d/99-hardening.conf applied. kptr_restrict=2, rp_filter=1, accept_redirects=0, SYN cookies, core dump restriction.

■■■■■■■■■■ 10/10

P1

auditd — /etc/audit/rules.d/key-access.rules monitoring age key, SSH keys (d000, d001, github), authorized_keys, GPG private keys.

■■■■■■■■■■ 10/10

P1

Agent socket cleanup — 304 stale sockets purged, 103 current preserved.

■■■■■■■■■■ 10/10

P2

AppArmor profiles for age and ssh-agent — complain-first, enforce after validation

□□□□□□□□□□ 0/10

P2

Age identity compartmentalization

□□□□□□□□□□ 0/10

P2

Wazuh agent enrollment — FIM on key directories, centralized log analysis, CVE scanning

□□□□□□□□□□ 0/10

P3

arch-audit integration into daily timer

□□□□□□□□□□ 0/10

P3

Package audit and removal

□□□□□□□□□□ 0/10

Deep Audit Findings — 2026-06-24 21:22

Full system sweep using security-audit-commands.adoc. Results below.

Immediate Action Required
Severity Finding Remediation

HIGH ■■■■■■■■■■ 10/10

d001 plaintext in /tmp — RESOLVED. 15 decrypted work project files removed from /tmp/d001-backup-mschapv2-to-cert-migration-203522/.

rm -rf /tmp/d001-backup-mschapv2-to-cert-migration-203522/

HIGH ■■■■■■■■■■ 10/10

Coredumps disabledulimit -c returns unlimited. Despite fs.suid_dumpable=0 in sysctl, user processes can still dump core. A core dump of age or ssh-agent could contain key material in cleartext.

# Disable coredump storage
sudo sed -i '/^\[Coredump\]$/a Storage=none\nProcessSizeMax=0' \
  /etc/systemd/coredump.conf

# Enforce hard limit
echo '* hard core 0'

sudo tee -a /etc/security/limits.conf

# Verify (open a NEW terminal for ulimit to reflect) cat /etc/systemd/coredump.conf

grep -vE '^#

^$' cat /etc/security/limits.conf

tail -3 ulimit -c ----

MEDIUM ■■■■■■■■■■ 10/10

GPG agent hardened — cache TTL reduced to 15min/30min. allow-preset-passphrase removed. Pinentry synced. Deduplicated allow-loopback-pinentry. Stowed from dots-quantum — symlink active. Validated: gopass decrypt OK, git push OK.

Paste 1 — harden the dots-quantum source (review AFTER output before proceeding)
DQ=~/atelier/_projects/personal/dots-quantum/gpg/.gnupg/gpg-agent.conf

echo "=== BEFORE ===" && cat "$DQ"

# Cache TTL — 15min idle / 30min max
sed -i 's/default-cache-ttl 7200/default-cache-ttl 900/'         "$DQ"
sed -i 's/max-cache-ttl 28800/max-cache-ttl 1800/'               "$DQ"
sed -i 's/default-cache-ttl-ssh 7200/default-cache-ttl-ssh 900/' "$DQ"
sed -i 's/max-cache-ttl-ssh 28800/max-cache-ttl-ssh 1800/'       "$DQ"

# Remove pivot risk
sed -i '/^allow-preset-passphrase$/d' "$DQ"

# Sync pinentry with current workstation
sed -i 's

pinentry-program /usr/bin/pinentry-qt

pinentry-program /home/evanusmodestus/.local/bin/pinentry-auto

' "$DQ"

# Deduplicate + add single loopback-pinentry sed -i '/^allow-loopback-pinentry$/d' "$DQ" echo 'allow-loopback-pinentry' >> "$DQ"

# Update comment sed -i 's/Cache TTL in seconds (2 hours default, 8 hours max)/Cache TTL in seconds (15min idle, 30min max)/' "$DQ"

echo "=== AFTER ===" && cat "$DQ" ----

.Paste 2 — backup, stow, reload (only after verifying paste 1) [source,bash] ---- cp ~/.gnupg/gpg-agent.conf ~/.gnupg/gpg-agent.conf.bak.$(date +%F) rm ~/.gnupg/gpg-agent.conf stow -d ~/atelier/_projects/personal/dots-quantum -t ~ gpg

ls -la ~/.gnupg/gpg-agent.conf gpg-connect-agent reloadagent /bye grep -E 'cache-ttl

preset

loopback' ~/.gnupg/gpg-agent.conf ----

.Paste 3 — validate (run after stow + reload) [source,bash] ---- # 1. Symlink is active ls -la ~/.gnupg/gpg-agent.conf

grep dots-quantum && echo "GOOD: stow symlink"

# 2. GPG decrypt works (gopass → pinentry → GPG agent) gopass show v3/domains/d000/identity/ssh/id_ed25519_d000 > /dev/null && echo "GOOD: GPG decrypt"

# 3. Git push auth works (SSH key) git -C ~/atelier/_bibliotheca/domus-captures push --dry-run origin main 2>&1

grep -q 'up-to-date|Everything' && echo "GOOD: git push"

# 4. Settings applied grep -E 'cache-ttl

preset

loopback' ~/.gnupg/gpg-agent.conf # Expected: 900, 1800, no preset-passphrase, one allow-loopback-pinentry

# 5. No leftover comment grep -c 'Allow preset passphrase' ~/.gnupg/gpg-agent.conf

grep -q '0' && echo "GOOD: cleaned up" ----

.Backout — restore previous config if anything breaks [source,bash] ---- # 1. Remove the stow symlink stow -d ~/atelier/_projects/personal/dots-quantum -t ~ -D gpg

# 2. Restore the backup cp ~/.gnupg/gpg-agent.conf.bak.$(date +%F) ~/.gnupg/gpg-agent.conf

# 3. Reload gpg-connect-agent reloadagent /bye

# 4. Verify restored grep -E 'cache-ttl

preset

loopback' ~/.gnupg/gpg-agent.conf

# 5. Revert dots-quantum source git -C ~/atelier/_projects/personal/dots-quantum checkout — gpg/.gnupg/gpg-agent.conf ----

MEDIUM ■■■■■■■■■■ 10/10

pam_wheel enforcedauth required pam_wheel.so use_uid uncommented. Only wheel group can su.

Completed — no further action.

MEDIUM ■■■■■■■■■■ 10/10

securetty emptied — root console login blocked on all TTYs. sudo and su confirmed working.

Completed — no further action.

MEDIUM

PASS_MAX_DAYS 99999 — Passwords never expire in /etc/login.defs. Acceptable for key-only SSH, but local sudo password should rotate.

Review and set: sudo sed -i 's/PASS_MAX_DAYS\t99999/PASS_MAX_DAYS\t365/' /etc/login.defs

MEDIUM ■■■■■■■■■■ 10/10

USBGuard installed and active — policy generated from connected devices (dock, YubiKeys, fingerprint, camera, Bluetooth, ZFold7). Programmatic allow/block pipeline tested.

Paste 1 — install
sudo pacman -S usbguard
Paste 2 — connect ALL devices first, then generate policy

Plug in your dock, YubiKeys, external drives — everything you normally use. The policy whitelists only what’s connected at generation time. Anything not captured will be blocked.

sudo usbguard generate-policy > /tmp/usbguard-rules.conf
cat /tmp/usbguard-rules.conf

Review the output — you should see your xHCI controllers (internal), dock hubs, YubiKeys (1050:0407), ethernet adapter, camera, fingerprint reader, Bluetooth, etc. If a device is missing, plug it in and regenerate with `>

` to overwrite:

[source,bash] ---- sudo usbguard generate-policy

/tmp/usbguard-rules.conf cat /tmp/usbguard-rules.conf ----

.Paste 3 — apply and enable (only after reviewing the policy above) [source,bash] ---- sudo cp /tmp/usbguard-rules.conf /etc/usbguard/rules.conf sudo systemctl enable --now usbguard sudo usbguard list-devices ----

.Paste 4 — authorize new devices later (plug in, then allow programmatically) [source,bash] ---- # See what was blocked sudo usbguard list-devices

grep block ----

[source,bash] ---- # Allow all blocked devices (--no-run-if-empty guards against empty input) sudo usbguard list-devices

grep block

awk -F: '{print $1}'

xargs -n1 --no-run-if-empty sudo usbguard allow-device

# Verify sudo usbguard list-devices

grep -i allow

tail -5 ----

[source,bash] ---- # Persist — append permanent rule by vendor:product ID sudo usbguard append-rule 'allow id <vendor>:<product>'

# Verify rule was added sudo tail -3 /etc/usbguard/rules.conf ----

.Block a device programmatically (for testing or incident response) [source,bash] ---- # Block by name pattern sudo usbguard block-device "$(sudo usbguard list-devices

grep -i '<name>'

awk -F: '{print $1}')"

# Verify sudo usbguard list-devices

grep block ----

.Backout — if a USB device stops working [source,bash] ---- sudo systemctl stop usbguard sudo systemctl disable usbguard ----

LOW

Thunderbolt module loaded — DMA attack vector. boltctl not installed, no authorization policy.

Paste 1 — install
sudo pacman -S bolt
Paste 2 — review connected devices and set policy
boltctl list
boltctl monitor

INFO

Docker running — daemon active with bridge networks and NAT rules. Docker group grants root-equivalent access. Verify membership: getent group docker

Review whether Docker is needed. Consider rootless Podman as replacement.

Confirmed Good (Deep Audit)
Check Result

SSH hardening

Include directive present in sshd_config. Drop-in loaded. All settings verified in sshd -T runtime output.

PAM faillock

Active in system-authpreauth + authfail + authsucc chain. Account lockout on repeated failures.

SUID binaries

48 total, all standard (sudo, passwd, mount, su, pkexec, chrome-sandbox, nvidia-modprobe). Duplicates in btrfs snapshot .snapshots/pre-psr-fix-2026-06-19/ — expected.

Key permissions

All SSH and age keys at 600, owned by evanusmodestus:evanusmodestus. No drift.

Home directory

700 — correct.

Filesystem mounts

/tmp is tmpfs with nosuid,nodev. /dev/shm has nosuid,nodev.

Sysctl hardening

All params applied: kptr_restrict=2, rp_filter=1, accept_redirects=0, tcp_syncookies=1, suid_dumpable=0.

Kernel boot params

AppArmor enabled via cmdline: lsm=landlock,lockdown,yama,integrity,apparmor,bpf apparmor=1 security=apparmor. LUKS via cryptdevice=UUID:cryptroot.

Wayland

XDG_SESSION_TYPE=wayland via Hyprland. Clipboard/keylogger isolation enforced. Xwayland running for compatibility but sandboxed.

DNS

Pointing to own BIND servers (10.50.1.90, 10.50.1.91). No external resolvers.

Auditd

6 rules loaded. Already caught Claude Code’s ripgrep touching key files (expected — rg --files --follow --hidden).

fail2ban

Active, 0 bans, 0 failures.

AppArmor

162 profiles loaded, 82 enforced. Docker default profile active.

Clipboard

No clipboard manager detected. Gopass show -c 45-second clear is effective.

XDG_RUNTIME_DIR

700, correct owner.

Shell configs

No inline credentials. SOPS_AGE_KEY_FILE points to path (not inline key). HISTORY_IGNORE filters sensitive patterns (Authorization, API_TOKEN, PASSWORD).

Gopass stores

Encrypted at rest in ~/.local/share/gopass/stores/{root,v2,v3}.

GitHub CLI

~/.config/gh/hosts.yml at 600 permissions.

Git credential helper

Not set to store (no plaintext credential file).

.env files

None found in home directory.

Kernel modules

usb-storage, firewire-core, cramfs, freevxfs, jffs2, hfs, hfsplus, udf — all not loaded. Only thunderbolt loaded (has physical port).

Decision Needed

Remove Arch Linux documentation pages? No. The vulnerability is in the packages, not the documentation. Removing pages about Arch from domus-captures would lose institutional knowledge. The fix is hardening the system, not redacting references to it.

  • 2026-06-20: SSH modular config.d/ architecture built — replaced monolithic config with 7 numbered files

  • 2026-06-21: ssh-config-activation.adoc — config.d activation, 80-d000-hosts.conf populated from DNS via dig loop, Cisco device pattern fix, stow deployment pattern, ARS-SEC-SSH-001 v3.0

  • 2026-06-21: vault-ssh-cert-renewal.adoc — Vault SSH certificate lifecycle, 8h TTL renewal, systemd timer automation

  • 2026-06-22: cve-2026-55200-libssh2.adoc + cve-2026-55200-mitigation.adoc — libssh2 CVE mitigated via curlrc protocol block

  • 2026-06-22: ssh-config-other-machines.adoc — config.d deployed to additional machines

  • 2026-06-23: ssh-config-missing-hosts.adoc — host completeness audit

  • 2026-06-17: ssh-gopass-workflow.adoc — post-quantum hardened defaults (ML-KEM-768 + NTRU Prime KEX)

  • RCA-2026-03-16-002: SSH Config Order — root cause that drove the modular architecture

DevSecOps Portfolio Mapping

Every action in this assessment maps to a demonstrable DevSecOps competency. This is not academic — it’s hands-on evidence built on production-grade infrastructure.

Today’s Action DevSecOps Skill Interview-Ready Narrative

SSH daemon hardening via sshd_config.d drop-in

Infrastructure hardening, CIS benchmark alignment

"I harden SSH daemons using modular drop-in configs — password auth disabled, AllowUsers scoped, MaxAuthTries 3, agent forwarding off. Verify-before/change/verify-after pattern."

fail2ban with systemd backend

Host-based intrusion prevention (HIPS)

"I deploy fail2ban with systemd journal integration — 3-strike policy, 1h bans. Complements nftables default-drop firewall."

sysctl.d kernel hardening

Linux security tuning, attack surface reduction

"I apply kernel-level hardening: kptr_restrict=2 to prevent KASLR bypass, rp_filter=1 against IP spoofing, ICMP redirect suppression, SYN cookie protection."

AppArmor complain-first workflow

Mandatory Access Control (MAC), policy-as-code

"I write custom AppArmor profiles for sensitive binaries (age, ssh-agent) using a complain-first methodology — log violations without blocking, validate against real workflows, then enforce."

auditd key file monitoring

Security monitoring, audit trail design

"I configure auditd to watch private key files (SSH, age, GPG) for unauthorized reads — creating an audit trail that feeds into centralized SIEM."

Wazuh SIEM on k3s

SIEM deployment, Kubernetes operations, centralized logging

"I operate a Wazuh SIEM on k3s with Vault-signed TLS certs, NFS-backed persistent storage, MetalLB load balancing, and Cilium CNI. File integrity monitoring on key material, custom decoders for mail security."

CVE triage with arch-audit

Vulnerability management lifecycle

"I maintain a vulnerability management process — arch-audit for package-level CVE detection, integrated into daily systemd timers for continuous monitoring."

Post-quantum SSH KEX

Cryptographic modernization, forward secrecy

"I deploy post-quantum key exchange (ML-KEM-768 + NTRU Prime) on all SSH connections, with Vault-signed 8-hour certificates for short-lived credential rotation."

Vault SSH certificate lifecycle

Secrets management, PKI, zero-trust principles

"I use HashiCorp Vault to sign SSH certificates with 8-hour TTLs, automated via systemd timers. No static keys on servers — principals-based access."

Age identity compartmentalization

Encryption key management, least-privilege

"I compartmentalize encryption keys by domain — infrastructure, personal, work — so compromise of one key doesn’t expose all encrypted data."

Modular SSH config.d architecture

Configuration management, Infrastructure as Code

"I designed a modular SSH config architecture with numbered priority files, age-encrypted host mappings, and stow-based deployment across workstations."

nftables default-drop firewall

Network security, defense in depth

"I maintain nftables with a default-drop input policy — only SSH exposed. All other services bound to loopback."

Key differentiator: This isn’t "I took a course." It’s "I built, operate, and document a multi-layer security stack on infrastructure I own." The documentation itself (Antora, AsciiDoc, case studies, RCAs) demonstrates technical writing — another DevSecOps core competency.

Security Audit Commands — Deep System Sweep

Security Audit Commands — Deep System Sweep

Run each block, read the output, understand what it tells you. Grouped by attack surface.

1. SSH Daemon — Verify Hardening Applied
# Confirm the Include directive exists (without it, the drop-in is ignored)
grep -i '^Include' /etc/ssh/sshd_config

# Read the drop-in you created
cat /etc/ssh/sshd_config.d/10-hardening.conf

# Effective runtime config — what sshd actually uses
sudo sshd -T | grep -iE 'permit|password|pubkey|agent|x11|maxauth|grace|allowusers'
2. PAM — Authentication Stack
# Is pam_faillock configured? (locks accounts after N failures)
grep -r 'pam_faillock' /etc/pam.d/

# Is pam_wheel required for su? (restricts su to wheel group only)
grep 'pam_wheel' /etc/pam.d/su

# Faillock settings — how many attempts before lockout?
cat /etc/security/faillock.conf | grep -vE '^#|^$'

# What auth methods does sshd use?
cat /etc/pam.d/sshd

# What does system-auth look like? (core auth chain)
cat /etc/pam.d/system-auth | grep -vE '^#|^$'
3. Password and Login Policy
# Password aging, UMASK, encryption method
grep -E 'PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE|UMASK|ENCRYPT_METHOD|LOGIN_RETRIES|LOGIN_TIMEOUT' /etc/login.defs | grep -vE '^#'

# Password history — prevent reuse
cat /etc/security/pwhistory.conf | grep -vE '^#|^$'

# Account lockout limits
cat /etc/security/limits.conf | grep -vE '^#|^$'

# Does securetty exist? (empty = blocks root on console)
cat /etc/securetty 2>/dev/null || echo "securetty does not exist"
4. Privilege Escalation
# SUID binaries — count and list unusual ones
find / -perm /4000 -type f 2>/dev/null | wc -l
find / -perm /4000 -type f 2>/dev/null | sort

# SGID binaries
find / -perm /2000 -type f 2>/dev/null | wc -l

# Combined SUID+SGID
find / -perm /6000 -type f 2>/dev/null | wc -l

# Sudoers — is NOPASSWD set anywhere? (dangerous)
sudo grep -r 'NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null

# Who is in the wheel group?
getent group wheel

# Can any user sudo without restriction?
sudo cat /etc/sudoers | grep -vE '^#|^$|^Defaults'
5. Filesystem Permissions
# World-writable directories (outside /tmp, /proc, /sys)
find / -type d -perm -0002 -not -path '/tmp/*' -not -path '/proc/*' \
  -not -path '/sys/*' -not -path '/dev/*' -not -path '/run/*' 2>/dev/null | head -20

# Is /tmp mounted with noexec,nosuid,nodev?
mount | grep '/tmp'
grep '/tmp' /etc/fstab

# Is /dev/shm restricted?
mount | grep '/dev/shm'

# Home directory permissions (should be 700)
stat -c '%a %n' /home/*

# Verify key file permissions haven't drifted
stat -c '%a %U:%G %n' ~/.ssh/id_* ~/.age/identities/* 2>/dev/null
6. Kernel and Boot Security
# Kernel command line — security-relevant boot params
cat /proc/cmdline

# Are dangerous modules blacklisted?
ls /etc/modprobe.d/
cat /etc/modprobe.d/*.conf 2>/dev/null | grep -i 'blacklist\|install.*bin.*true'

# Modules to consider blacklisting (if unused):
# usb-storage, firewire-core, thunderbolt, cramfs, freevxfs, jffs2, hfs, hfsplus, udf
for mod in usb-storage firewire-core thunderbolt cramfs freevxfs jffs2 hfs hfsplus udf; do
  lsmod | grep -q "$mod" && echo "LOADED: $mod" || echo "not loaded: $mod"
done

# Current sysctl hardening — verify your 99-hardening.conf is applied
sysctl kernel.kptr_restrict kernel.yama.ptrace_scope kernel.dmesg_restrict \
  net.ipv4.conf.all.rp_filter net.ipv4.conf.all.accept_redirects \
  net.ipv4.tcp_syncookies fs.suid_dumpable

# Coredump config — should be disabled or restricted
cat /etc/systemd/coredump.conf | grep -vE '^#|^$'
ulimit -c
7. Network Exposure
# All listening TCP ports with process names
ss -tlnp

# All listening UDP ports
ss -ulnp

# Listening Unix domain sockets (unusual ones = investigation targets)
ss -xlnp | head -30

# Firewall rules — verify default-drop
sudo nft list ruleset

# DNS resolver — is it using encrypted DNS?
cat /etc/resolv.conf

# TCP wrappers (if installed)
cat /etc/hosts.allow 2>/dev/null
cat /etc/hosts.deny 2>/dev/null

# NetworkManager profiles — any open WiFi auto-connect?
nmcli connection show 2>/dev/null | head -20
ls /etc/NetworkManager/system-connections/ 2>/dev/null
8. USB and Physical Access
# USBGuard — is it installed? (blocks unauthorized USB devices)
pacman -Q usbguard 2>/dev/null || echo "USBGuard not installed"

# Thunderbolt policy
pacman -Q bolt 2>/dev/null && boltctl list 2>/dev/null || echo "boltctl not installed"

# Is screen locking configured? (you use hyprlock)
pacman -Q hyprlock 2>/dev/null
grep -r 'lock' /etc/pam.d/hyprlock 2>/dev/null
9. User-Level Attack Surface
# Sensitive environment variables (NAMES only — never values)
env | grep -iE 'token|key|secret|pass|auth|api' | awk -F= '{print $1}'

# Plaintext credentials in shell configs
grep -rnE 'token|password|secret|api_key|API_KEY' ~/.bashrc ~/.zshrc ~/.zprofile 2>/dev/null

# GitHub CLI credentials
ls -la ~/.config/gh/hosts.yml 2>/dev/null
# DO NOT cat this — just check it exists and permissions
stat -c '%a' ~/.config/gh/hosts.yml 2>/dev/null

# Git credential storage — should NOT be store (plaintext)
git config --global credential.helper

# .env files in home (potential credential leaks)
find ~ -maxdepth 4 -name '.env' -o -name '.env.local' -o -name '.env.production' 2>/dev/null

# Clipboard manager running? (captures gopass copies)
pgrep -a 'clipman\|clipcat\|copyq\|gpaste\|klipper' 2>/dev/null || echo "No clipboard manager detected"

# GPG agent cache TTL (how long before keys are flushed from memory)
grep -i 'default-cache-ttl\|max-cache-ttl' ~/.gnupg/gpg-agent.conf 2>/dev/null

# Gopass store location and encryption
gopass config 2>/dev/null | grep -i 'path\|mount'

# XDG_RUNTIME_DIR permissions (should be 700, owned by you)
stat -c '%a %U:%G %n' "$XDG_RUNTIME_DIR" 2>/dev/null

# Detached tmux sessions (visible content if stolen)
tmux list-sessions 2>/dev/null

# Display server — X11 has clipboard/keylogger surface, Wayland is sandboxed
echo "$XDG_SESSION_TYPE"
10. Service Hardening
# Which user systemd services are running?
systemctl --user list-units --type=service --state=running --no-pager

# Check a service for hardening directives (pick your security-remediation service)
systemctl --user cat security-remediation.service 2>/dev/null

# System services — anything unexpected?
systemctl list-units --type=service --state=running | grep -vE 'systemd|dbus|polkit|audit|apparmor|fail2ban|sshd|NetworkManager'

# Flatpak — sandboxed apps
flatpak list 2>/dev/null | head -10
flatpak permissions 2>/dev/null | head -20
11. File Integrity and Monitoring
# Recently modified files in sensitive directories (last 24h)
find ~/.ssh/ -mtime -1 -type f 2>/dev/null
find ~/.age/ -mtime -1 -type f 2>/dev/null
find ~/.gnupg/ -mtime -1 -type f 2>/dev/null
find /etc/ssh/ -mtime -1 -type f 2>/dev/null

# auditd — verify rules are loaded
sudo auditctl -l

# Recent key access events
sudo ausearch -k ssh_key_read --interpret 2>/dev/null | tail -10
sudo ausearch -k age_key_read --interpret 2>/dev/null | tail -10
sudo ausearch -k authorized_keys_mod --interpret 2>/dev/null | tail -10

# fail2ban status
sudo fail2ban-client status sshd

# AppArmor — what's enforced vs complain vs unconfined
sudo aa-status 2>/dev/null | head -40
12. Stale Secrets in /tmp and /dev/shm
# Anything sensitive left in temp directories?
find /tmp -maxdepth 2 -name '*.key' -o -name '*.pem' -o -name '*.age' \
  -o -name '*.gpg' -o -name '*secret*' -o -name '*credential*' 2>/dev/null

find /dev/shm -maxdepth 2 -type f 2>/dev/null

# Files owned by you in /tmp (review for cleanup)
find /tmp -user "$(whoami)" -type f 2>/dev/null | head -20
13. Package Vulnerability Scan
# Known-vulnerable packages
arch-audit

# Orphaned packages (installed as deps, no longer needed)
pacman -Qtd 2>/dev/null

# Explicitly installed package count (large = large attack surface)
pacman -Qe | wc -l

# Foreign packages (AUR — not in official repos, less audited)
pacman -Qm
How to Read the Results
  • SUID binaries: standard ones are /usr/bin/sudo, /usr/bin/passwd, /usr/bin/mount, etc. Anything outside /usr/bin/ or /usr/lib/ is suspicious.

  • World-writable dirs: only /tmp, /var/tmp, and specific app dirs should be writable. Anything else = investigate.

  • Listening ports: only SSH (22) should be on 0.0.0.0. Everything else should be 127.0.0.1.

  • env variables: if you see TOKEN or API_KEY set in your shell, trace where it’s sourced from. Should come from gopass or a secrets manager, not hardcoded in .zshrc.

  • Clipboard managers: if one is running, gopass show -c copies are logged. 45-second clear doesn’t help if the clipboard manager persists history.

  • X11 vs Wayland: if XDG_SESSION_TYPE returns x11, any process can keylog and read clipboard. Wayland isolates this.

Security Hardening — Deploy to Work Machine

Security Hardening — Deploy to Work Machine

This partial documents how to replicate the security hardening applied on the personal workstation (modestus-aw.inside.domusdigitalis.dev) to a work machine that shares the same dots-quantum git tree and GNU Stow workflow.

Two categories: stowed configs that travel via git, and system-level configs that must be applied manually per machine.

The work machine username may differ. Replace WORK_USER with the actual username on the target machine throughout.
Category 1: Stowed Configs (dots-quantum pull + restow)

These changes live in dots-quantum and deploy via git pull + stow.

GPG Agent Hardening

Changes made to gpg/.gnupg/gpg-agent.conf:

  • Cache TTL reduced to 900/1800 (15 min idle, 30 min max)

  • allow-preset-passphrase removed

  • Pinentry synced to pinentry-auto

  • allow-loopback-pinentry deduplicated (was listed twice)

# Pull latest dots-quantum
git -C ~/atelier/_projects/personal/dots-quantum pull
# Verify current state before restow
cat ~/.gnupg/gpg-agent.conf
# Backup existing config
cp ~/.gnupg/gpg-agent.conf ~/.gnupg/gpg-agent.conf.bak.$(date +%F)
# Remove symlink/file so stow can replace it cleanly
rm ~/.gnupg/gpg-agent.conf

# Restow GPG package
stow -d ~/atelier/_projects/personal/dots-quantum -t ~ gpg
# Reload agent with new config
gpg-connect-agent reloadagent /bye
# Verify the hardened values are active
grep -E 'cache-ttl|preset|loopback' ~/.gnupg/gpg-agent.conf

Expected output — two cache-ttl lines, no allow-preset-passphrase, single allow-loopback-pinentry:

default-cache-ttl 900
max-cache-ttl 1800
allow-loopback-pinentry
# Functional test — decrypt something to confirm pinentry works
gopass show v3/domains/d000/identity/ssh/id_ed25519_d000 > /dev/null && echo "GOOD: GPG decrypt works"
Category 2: System-Level Configs (manual per machine)

These live in /etc/ and are NOT tracked in dots-quantum. Each must be applied manually with verify-before/change/verify-after.

1. SSH Daemon Hardening

Drop-in file: /etc/ssh/sshd_config.d/10-hardening.conf

# Verify Include directive exists in main config (drop-ins are ignored without it)
grep -i '^Include' /etc/ssh/sshd_config
# Verify current state — should return nothing if drop-in doesn't exist yet
cat /etc/ssh/sshd_config.d/10-hardening.conf 2>/dev/null || echo "File does not exist yet"
# Create the drop-in — replace _WORK_USER_ with the actual username
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf << 'EOF'
# Security hardening — deployed from modestus-p16g baseline
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
AllowAgentForwarding no
AllowUsers _WORK_USER_
LogLevel VERBOSE
EOF
Test pubkey auth in a separate terminal before restarting sshd. If pubkey auth fails after restart, you lose SSH access.
# Dry-run: validate sshd config syntax
sudo sshd -t && echo "GOOD: sshd config valid" || echo "FAIL: fix config before restarting"
# Open a second SSH session to the work machine NOW — keep it open as a lifeline

# Only after confirming the second session works:
sudo systemctl restart sshd
# Verify — from the lifeline session
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|maxauthtries|allowagentforwarding|allowusers|loglevel'
2. fail2ban — SSH Jail
# Verify fail2ban is installed
pacman -Q fail2ban 2>/dev/null || echo "Not installed — run: sudo pacman -S fail2ban"
# Verify current state
cat /etc/fail2ban/jail.local 2>/dev/null || echo "File does not exist yet"
sudo tee /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 3600
backend = systemd
EOF
sudo systemctl enable --now fail2ban
# Verify jail is active
sudo fail2ban-client status sshd
3. Kernel Hardening via sysctl
# Verify current kernel parameters
sysctl kernel.kptr_restrict net.ipv4.conf.all.rp_filter net.ipv4.conf.all.accept_redirects net.ipv4.tcp_syncookies fs.suid_dumpable
sudo tee /etc/sysctl.d/99-hardening.conf << 'EOF'
# Kernel pointer restriction — block /proc/kallsyms leak
kernel.kptr_restrict = 2

# Reverse path filtering — drop spoofed source IPs
net.ipv4.conf.all.rp_filter = 1

# Reject ICMP redirects — prevent route poisoning
net.ipv4.conf.all.accept_redirects = 0

# SYN cookies — mitigate SYN flood
net.ipv4.tcp_syncookies = 1

# Disable SUID core dumps — prevent credential leak from coredumps
fs.suid_dumpable = 0
EOF
# Apply immediately
sudo sysctl --system
# Verify after
sysctl kernel.kptr_restrict net.ipv4.conf.all.rp_filter net.ipv4.conf.all.accept_redirects net.ipv4.tcp_syncookies fs.suid_dumpable
4. auditd — Key File Access Monitoring
# Verify auditd is installed
pacman -Q audit 2>/dev/null || echo "Not installed — run: sudo pacman -S audit"
# Verify current state
cat /etc/audit/rules.d/key-access.rules 2>/dev/null || echo "File does not exist yet"
# Create audit rules — adjust paths if key locations differ on work machine
sudo tee /etc/audit/rules.d/key-access.rules << EOF
# age identity
-w /home/_WORK_USER_/.age/identities/personal.key -p rwa -k age-key-access

# SSH private keys — adjust names to match work machine keys
-w /home/_WORK_USER_/.ssh/id_ed25519_d000 -p rwa -k ssh-key-access
-w /home/_WORK_USER_/.ssh/id_ed25519_d001 -p rwa -k ssh-key-access
-w /home/_WORK_USER_/.ssh/id_ed25519_github -p rwa -k ssh-key-access

# authorized_keys
-w /home/_WORK_USER_/.ssh/authorized_keys -p rwa -k ssh-authkeys

# GPG private keys
-w /home/_WORK_USER_/.gnupg/private-keys-v1.d/ -p rwa -k gpg-key-access
EOF
sudo systemctl enable --now auditd
sudo augenrules --load
# Verify rules loaded
sudo auditctl -l | grep -E 'age-key|ssh-key|ssh-authkeys|gpg-key'
5. Coredump Disable

Two files must be configured to fully disable core dumps.

# Verify current coredump config
cat /etc/systemd/coredump.conf | grep -E '^Storage|^ProcessSizeMax'
ulimit -c
# systemd coredump — set Storage=none and ProcessSizeMax=0
sudo sed -i 's/^#Storage=.*/Storage=none/' /etc/systemd/coredump.conf
sudo sed -i 's/^#ProcessSizeMax=.*/ProcessSizeMax=0/' /etc/systemd/coredump.conf
# PAM limits — block core dumps for all users
grep -q '^\* hard core 0' /etc/security/limits.conf || echo '* hard core 0' | sudo tee -a /etc/security/limits.conf
# Verify after
grep -E '^Storage|^ProcessSizeMax' /etc/systemd/coredump.conf
grep 'hard core' /etc/security/limits.conf
6. pam_wheel — Restrict su to wheel Group
# Verify current state — look for the pam_wheel line
grep 'pam_wheel' /etc/pam.d/su
# Uncomment the pam_wheel line (if commented)
sudo sed -i 's/^#\(auth.*required.*pam_wheel\.so.*use_uid\)/\1/' /etc/pam.d/su
# Verify after — should show uncommented line
grep 'pam_wheel' /etc/pam.d/su
# Confirm your user is in the wheel group
groups _WORK_USER_ | grep -q wheel && echo "GOOD: user in wheel group" || echo "FAIL: add user to wheel group first"
7. securetty — Block Console Root Login
# Verify current state
wc -l /etc/securetty 2>/dev/null || echo "File does not exist (may already be safe)"
# Backup then truncate
sudo cp /etc/securetty /etc/securetty.bak.$(date +%F) 2>/dev/null
sudo truncate -s 0 /etc/securetty
# Verify — should be 0 bytes
wc -c /etc/securetty
Validation Checklist

Run this block after all changes are applied. Every line should output a confirmation.

echo "=== Security Hardening Validation ==="

# 1. GPG agent — hardened cache TTL
echo "--- GPG Agent ---"
grep -c 'default-cache-ttl 900' ~/.gnupg/gpg-agent.conf && echo "PASS: cache-ttl 900" || echo "FAIL: cache-ttl"
grep -c 'max-cache-ttl 1800' ~/.gnupg/gpg-agent.conf && echo "PASS: max-cache-ttl 1800" || echo "FAIL: max-cache-ttl"
grep -c 'allow-preset-passphrase' ~/.gnupg/gpg-agent.conf && echo "FAIL: allow-preset-passphrase still present" || echo "PASS: allow-preset-passphrase removed"

# 2. SSH daemon
echo "--- SSH Daemon ---"
sudo sshd -T 2>/dev/null | grep -q 'permitrootlogin no' && echo "PASS: PermitRootLogin no" || echo "FAIL: PermitRootLogin"
sudo sshd -T 2>/dev/null | grep -q 'passwordauthentication no' && echo "PASS: PasswordAuthentication no" || echo "FAIL: PasswordAuthentication"
sudo sshd -T 2>/dev/null | grep -q 'maxauthtries 3' && echo "PASS: MaxAuthTries 3" || echo "FAIL: MaxAuthTries"

# 3. fail2ban
echo "--- fail2ban ---"
sudo fail2ban-client status sshd > /dev/null 2>&1 && echo "PASS: sshd jail active" || echo "FAIL: sshd jail"

# 4. sysctl
echo "--- Kernel Hardening ---"
[ "$(sysctl -n kernel.kptr_restrict)" = "2" ] && echo "PASS: kptr_restrict=2" || echo "FAIL: kptr_restrict"
[ "$(sysctl -n net.ipv4.conf.all.rp_filter)" = "1" ] && echo "PASS: rp_filter=1" || echo "FAIL: rp_filter"
[ "$(sysctl -n net.ipv4.conf.all.accept_redirects)" = "0" ] && echo "PASS: accept_redirects=0" || echo "FAIL: accept_redirects"
[ "$(sysctl -n net.ipv4.tcp_syncookies)" = "1" ] && echo "PASS: tcp_syncookies=1" || echo "FAIL: tcp_syncookies"
[ "$(sysctl -n fs.suid_dumpable)" = "0" ] && echo "PASS: suid_dumpable=0" || echo "FAIL: suid_dumpable"

# 5. auditd
echo "--- auditd ---"
sudo auditctl -l 2>/dev/null | grep -q 'age-key-access' && echo "PASS: age key audit rule" || echo "FAIL: age key audit"
sudo auditctl -l 2>/dev/null | grep -q 'ssh-key-access' && echo "PASS: SSH key audit rule" || echo "FAIL: SSH key audit"
sudo auditctl -l 2>/dev/null | grep -q 'gpg-key-access' && echo "PASS: GPG key audit rule" || echo "FAIL: GPG key audit"

# 6. coredump
echo "--- Coredump ---"
grep -q '^Storage=none' /etc/systemd/coredump.conf && echo "PASS: coredump storage=none" || echo "FAIL: coredump storage"
grep -q '^\* hard core 0' /etc/security/limits.conf && echo "PASS: limits.conf core=0" || echo "FAIL: limits.conf"

# 7. pam_wheel
echo "--- pam_wheel ---"
grep -q '^auth.*required.*pam_wheel.so.*use_uid' /etc/pam.d/su && echo "PASS: pam_wheel enabled" || echo "FAIL: pam_wheel"

# 8. securetty
echo "--- securetty ---"
[ "$(wc -c < /etc/securetty 2>/dev/null)" = "0" ] && echo "PASS: securetty empty" || echo "WARN: securetty not empty or missing"

echo "=== Done ==="

grep -P Regex — Finding Multi-Remote Push Patterns

grep -P Regex — Finding Multi-Remote Push Patterns

Used three regex patterns to locate scripts that push to multiple git remotes. Each pattern targets a different aspect of the parallel push pattern.

Pattern 1 — Find scripts with push + background or xargs + push
grep -rnP 'push.*&|xargs.*push|for.*remote.*push|REMOTES.*\(' $(find . -name '*.sh' -type f)
Table 2. Regex breakdown
Token Matches

push.*&

push followed by anything then & — background push (git push origin main &)

`\pass:[

]`

PCRE alternation — OR between patterns (grep -P uses `\pass:[

]` as literal, `pass:[

]` as alternation)

xargs.*push

xargs piping into a push command

for.*remote.*push

A for loop iterating over remotes with push inside

REMOTES.*\(

-P enables Perl-compatible regex. With -P, | is alternation (no backslash needed). With -E (extended), same behavior. With basic grep (no flag), you’d need \| for alternation.
Pattern 2 — Find shell scripts by filename regex, then search content
find . -type f -regex '.*\(push\|remote\|gpall\).*\.sh' -exec grep -lP 'push.*main' {} +
Table 3. Regex breakdown
Token Matches

.*

Any characters (path prefix)

`\(push\pass:[

]remote\pass:[

]gpall\)`

POSIX basic regex group with alternation — matches filenames containing push, remote, or gpall

.*\.sh

Ends with .sh\. escapes the literal dot

find -regex uses POSIX basic regex by default — grouping requires \( \) and alternation requires \|. To use extended regex (unescaped parens/pipes), add -regextype posix-extended.

Pattern 3 — Find parallel push pattern across entire atelier
grep -rnP 'git.*push.*&|wait$' $(find ~/atelier -name '*.sh' -type f 2>/dev/null)
Table 4. Regex breakdown
Token Matches

git.push.&

git followed by push followed by & — the exact parallel push pattern

wait$

wait at end of line — the synchronization point after background pushes

2>/dev/null

Suppress find permission errors (not regex — shell redirection)

Results — the real files (excluding snapshots)

The three grep patterns found the parallel push pattern in these actual scripts:

File Pattern

git-push-session-partials.sh

REMOTES=(origin gitlab gitea) + git push "$remote" "$BRANCH" & + wait

domus-linux-ops/…​/domus-push-all.sh

git -C "$repo" push "$remote" main — sequential per-repo multi-remote

dots-quantum/…​/bash.sh

gpall() function — & wait pattern for parallel push

Noise reduction — exclude snapshots

The raw search hit 100+ matches in shell-snapshots (Claude Code session captures). Filter them out:

Exclude snapshots with grep -v
grep -rnP 'git.*push.*&|wait$' $(find ~/atelier -name '*.sh' -type f 2>/dev/null) | grep -v 'shell-snapshots'
Exclude with find
grep -rnP 'git.*push.*&|wait$' $(find ~/atelier -name '*.sh' -not -path '*/shell-snapshots/*' -type f 2>/dev/null)
Null-delimited safe version (handles spaces in paths)
find ~/atelier -name '*.sh' -not -path '*/shell-snapshots/*' -not -path '*/node_modules/*' -type f -print0 | xargs -0 grep -rnP 'git.*push.*&|wait$'

The -not -path predicates prune noisy directories before grep ever sees them — faster and cleaner than piping through grep -v after.

Regex quick reference — grep flavors
Feature Basic (grep) Extended (grep -E) PCRE (grep -P)

Alternation

`\pass:[

]`

`\pass:[

]`

`\pass:[

]`

Grouping

\( \)

( )

( )

One or more

\+

+

+

Zero or one

\?

?

?

Word boundary

\b (GNU)

\b (GNU)

\b

Lookahead

N/A

N/A

(?=…​) (?!…​)

Lookbehind

N/A

N/A

(?⇐…​) (?<!…​)

Non-greedy

Rule of thumb: use -P (PCRE) when you need lookahead/lookbehind or non-greedy. Use -E for everything else. Avoid basic regex — the escaping is backwards and error-prone.

The composition

The three commands demonstrate the same investigation pattern at different scopes:

Pattern 1:  grep content of .sh files in current directory
Pattern 2:  find files by name regex, then grep content
Pattern 3:  find files across entire atelier, then grep content

Each level widens the search aperture. The regex stays the same — only the find scope changes. That’s composition: same tools, different arrangement, different result.

Ad-Hoc Requests

Ad-Hoc Requests

Capture walk-ups, Teams pings, and unplanned work here.

*


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. 06-15: Assembler build fixed (26 errors → 0). Segmentation document extension to 06-16. d001 open linux-research to work on it.

  • 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)

Day-specific accomplishments here. === tmux-quantum Session Restructure — Remaining Tasks

Post-restructure tasks that require manual execution. All paths use $HOME for portability (/home/evanusmodestus).

1. Include Partials in Today’s Worklog

Five partials were created during this session. Add them to the worklog using sed -i pattern-addressed append.

# Set the target file
file="$HOME/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc"

# Verify the file exists and find the anchor line
awk '/Day-specific/{print NR": "$0}' "$file"

# Idempotent append — grep -q guard prevents duplicates
# NOTE: sed expression uses single-quote/double-quote concatenation
#   'literal $worklog'"$variable"'more literal'
# because partial$worklog contains a literal $ that double quotes
# would interpret as a shell variable (expanding to empty string)
for partial in \
  tmux-quantum-session-restructure \
  sed-append-cli-pattern \
  regex-pcre-grep-patterns \
  find-regex-path-matching \
  tmux-quantum-session-memory \
  tmux-quantum-session-remaining-tasks; do
    grep -q "$partial" "$file" || \
      sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/'"$partial"'.adoc[]' "$file"
done

# Verify — show the section after the anchor
awk '/Session Accomplishments/,/^---/' "$file"

2. Commit tmux-quantum Changes

# Verify what changed
git -C "$HOME/atelier/_projects/personal/tmux-quantum" status

# Stage and commit
git -C "$HOME/atelier/_projects/personal/tmux-quantum" add \
  sessions/ \
  && git -C "$HOME/atelier/_projects/personal/tmux-quantum" commit -m "$(cat <<'EOF'
refactor(sessions): data-driven restructure — 18 active, 16 archived

Audited all 25 sessions against 90-day git commit frequency.
Created 8 new sessions for active repos without coverage.
Merged cfg-nvim-lsp + train-nvim + test-nvim-plugins → cfg-nvim.
Fixed cfg-tmux (wrong source-file path), proj-netapi-tui (venv→uv),
lrn-spanish (blocking read -p, added edicion-analitica window).
Archived 16 zero-activity sessions to _archive/.
Added INDEX.adoc quick reference.
EOF
)"

3. Commit domus-captures Partials

# Verify
git -C "$HOME/atelier/_bibliotheca/domus-captures" status -- \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/

# Stage only this session's partials
git -C "$HOME/atelier/_bibliotheca/domus-captures" add \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/sed-append-cli-pattern.adoc \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/regex-pcre-grep-patterns.adoc \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/find-regex-path-matching.adoc \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-memory.adoc \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-remaining-tasks.adoc \
  docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc \
  && git -C "$HOME/atelier/_bibliotheca/domus-captures" commit -m "$(cat <<'EOF'
docs(worklog): tmux-quantum session restructure + CLI learning partials

Session partials: restructure summary, sed append patterns, PCRE regex
reference, find -regex path matching, Claude memory system, remaining tasks.
EOF
)"

4. Test New and Fixed Sessions

# Test one at a time — Ctrl-d or 'tmux kill-session -t NAME' to exit each
tmuxinator start cfg-nvim          # 3→1 merge
tmuxinator start cfg-tmux          # repointed to tmux-quantum
tmuxinator start proj-netapi       # new — ISE CLI tool
tmuxinator start proj-domus-build  # new — asciidoc build toolchain
tmuxinator start proj-domus-api    # new — FastAPI content API
tmuxinator start proj-assoc-engine # new — knowledge graph
tmuxinator start proj-domus-docs   # new — Antora aggregator
tmuxinator start proj-domus-spokes # new — multi-spoke workspace
tmuxinator start proj-crypta       # new — Rust roguelike

5. Validate All Sessions with yq

sessions_dir="$HOME/atelier/_projects/personal/tmux-quantum/sessions"

# Syntax validation
for f in "$sessions_dir"/*.yml; do
  yq '.' "$f" > /dev/null 2>&1 \
    && printf "OK   %s\n" "$(basename $f)" \
    || printf "FAIL %s\n" "$(basename $f)"
done

# Structural check — name, root, startup_window present
for f in "$sessions_dir"/*.yml; do
  name=$(yq -r '.name // "MISSING"' "$f")
  root=$(yq -r '.root // "MISSING"' "$f")
  startup=$(yq -r '.startup_window // "MISSING"' "$f")
  wcount=$(yq '.windows | length' "$f")
  printf "%-30s name=%-25s startup=%-15s windows=%s\n" \
    "$(basename $f)" "$name" "$startup" "$wcount"
done

# UPPERCASE name convention
yq -r '.name' "$sessions_dir"/*.yml | awk '$0 != toupper($0) {print "LOWERCASE: " $0}'

# Root paths exist on disk
for f in "$sessions_dir"/*.yml; do
  root=$(yq -r '.root' "$f" | sed "s|~|$HOME|")
  [ ! -d "$root" ] && printf "BAD ROOT: %-30s → %s\n" "$(basename $f)" "$root"
done

# No empty panes (panes that are just null or a comment)
for f in "$sessions_dir"/*.yml; do
  nulls=$(yq '[.windows[].[] | .panes[]? // . | select(. == null)] | length' "$f" 2>/dev/null)
  [ "$nulls" -gt 0 ] && printf "EMPTY PANES: %-30s %s null panes\n" "$(basename $f)" "$nulls"
done

Variable Reference

Variable Path

$HOME

/home/evanusmodestus

$HOME/atelier/_projects/personal/tmux-quantum

tmux-quantum repo root

$HOME/atelier/_bibliotheca/domus-captures

domus-captures repo root

$HOME/.config/tmuxinator

Symlink → tmux-quantum/sessions/

$HOME/.claude/projects/…​/memory/

Claude project-scoped memory

Claude Memory: CLI Learning Roadmap Created

During the tmux-quantum session restructure, a persistent memory entry was created to track CLI skill development areas for future sessions.

What Was Done

Created two files in Claude’s project-scoped memory system:

~/.claude/projects/-home-evanusmodestus-atelier--projects-personal-tmux-quantum/memory/
├── MEMORY.md                    # Index — pointers to memory files
├── project_testing_approach.md  # Existing — tmux-quantum deployment status
└── user_cli_learning_roadmap.md # NEW — regex/CLI skill gaps to target

How Claude Memory Works

Claude has a file-based memory system at ~/.claude/projects/<project-path>/memory/. Each memory is a markdown file with YAML frontmatter (name, description, type). MEMORY.md is an index that’s loaded into every conversation — it tells Claude which memory files exist and when to read them.

Memory types:

Type Purpose

user

Who you are, how you learn, what you know — tailors Claude’s responses

feedback

Corrections and confirmed approaches — prevents repeating mistakes

project

Ongoing work context, decisions, deadlines

reference

Pointers to external resources (Linear, Grafana, Slack channels)

The CLI learning roadmap was created as a user type memory because it describes how to tailor teaching to your current skill level and what areas to push on.

What the Memory Contains

Areas flagged for deliberate practice:

  • Regex (highest priority) — BRE vs ERE vs PCRE, lookaheads/lookbehinds, \K, character classes (\d, \w, \b), greedy vs non-greedy quantifiers, capture groups

  • find-regextype egrep -regex full-path matching, implicit anchoring, predicate composition

  • sed — pattern-addressed a\/i\/c\/d, hold buffer, verify-before/change/verify-after

  • awk — state machines, arrays, FNR vs NR for multi-file

  • Compositiongrep -q idempotent guards, tee stream splitting

Key observation recorded: two months in the terminal, already composing find/xargs/grep pipelines and reaching for alternation instinctively. The composability thinking is strong — syntax vocabulary is what needs deepening.

How to Interact with Memory

# View the memory index
cat ~/.claude/projects/-home-evanusmodestus-atelier--projects-personal-tmux-quantum/memory/MEMORY.md

# Read a specific memory
cat ~/.claude/projects/-home-evanusmodestus-atelier--projects-personal-tmux-quantum/memory/user_cli_learning_roadmap.md

# Ask Claude to remember something new
# Just say: "remember that I prefer X" or "save this for next time"

# Ask Claude to forget something
# Just say: "forget the thing about X"

Memory persists across conversations. Next time Claude Code opens in the tmux-quantum project, it reads MEMORY.md and knows what to drill on. === find -regex — Full Path Pattern Matching

Discovered during the 2026-06-24 session: using find -regex with alternation to locate both a date-named directory and topic-specific files in a single command.

What Was Done

find . -regex '.*\(2026-06-24\|tmux\.adoc\)'

Output:

./docs/modules/ROOT/pages/codex/terminal/tmux.adoc
./docs/modules/ROOT/pages/education/training/nvim/books/modern-vim/ch09-tmux.adoc
./docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-24

One command, two unrelated targets — a date directory and a topic file — found via regex alternation.

Why It Works

find -regex matches against the entire path from the search root, implicitly anchored (^…​$). That’s why .* is required at the front — without it, the pattern would need to match starting from ./docs/…​.

The escaping (\(, \|, \)) is required because find defaults to BRE (basic regex), where grouping and alternation metacharacters are literal unless escaped.

Cleaner Version — ERE Mode

find . -regextype egrep -regex '.*(2026-06-24|tmux\.adoc)'

-regextype egrep switches to extended regex — same as grep -E. No backslash escaping for (, |, ).

How find Matches Patterns

Option Pattern type Matches against

-name '*.adoc'

Shell glob

Basename only (tmux.adoc)

-path '*/2026-06-24'

Shell glob

Full path (./docs/…​/2026-06-24)

-regex '.*2026-06-24'

BRE (default)

Full path, implicitly anchored

-regextype egrep -regex

ERE

Full path, implicitly anchored

Key difference from grep: grep scans file contents for a pattern. find -regex matches path strings. They’re searching different things — grep reads bytes inside files, find reads the filesystem tree.

When to Use Each

Situation Tool

Find files by name pattern

find -name '*.adoc' (glob — simplest)

Find files by path structure

find -path '/worklog/' (glob on path)

Find with alternation or complex patterns

find -regextype egrep -regex '.*(a|b)'

Search file contents

grep -rP 'pattern' dir/

Find files by name THEN search contents

find -name '*.adoc' -exec grep -lP 'pattern' {} +

Combining with Other Predicates

find -regex composes with all other find predicates:

# Regex + time — today's matches modified in last hour
find . -regextype egrep -regex '.*(2026-06-24|tmux\.adoc)' -mmin -60

# Regex + type — only files (skip the 2026-06-24 directory)
find . -regextype egrep -regex '.*(2026-06-24|tmux\.adoc)' -type f

# Regex + exec — find and read
find . -regextype egrep -regex '.*(2026-06-24|tmux\.adoc)' -type f -exec head -3 {} +

Regex & PCRE Patterns with grep

Learned during the 2026-06-24 session: the regex mistake was not syntax — the alternation was correct — but location. The shell was in tmux-quantum/ while the target files lived in domus-captures/. Always verify where before debugging what.

The Three grep Regex Modes

Flag Mode Alternation syntax

(none)

Basic regex (BRE)

grep 'tmux-quantum|sed-append' — must escape |

-E

Extended regex (ERE)

grep -E 'tmux-quantum|sed-append' — unescaped |

-P

Perl-Compatible (PCRE)

grep -P 'tmux-quantum|sed-append' — plus lookaheads, \K, \d, \b

Rule of thumb: use -E for alternation and grouping. Upgrade to -P when you need lookaheads, lookbehinds, \K, or \d.

PCRE Lookaheads and Lookbehinds

Lookarounds assert what’s around the match without consuming characters. The match position stays where it is.

Positive Lookahead (?=…​)

Match X only if followed by Y:

# Match "tmux" only when followed by "-quantum"
grep -P 'tmux(?=-quantum)' sessions/*.yml

# Match "domus" only when followed by "-captures" or "-api"
grep -P 'domus(?=-(captures|api))' sessions/*.yml

The (?=…​) checks what’s ahead but doesn’t include it in the match. With -o:

grep -oP 'domus(?=-captures)' file.adoc
# Output: domus     (not domus-captures)
Negative Lookahead (?!…​)

Match X only if NOT followed by Y:

# Match "domus" NOT followed by "-captures" — finds domus-api, domus-docs, etc.
grep -P 'domus(?!-captures)' sessions/*.yml

# Find "include" directives that are NOT partials
grep -P 'include::(?!partial)' docs/modules/ROOT/pages/**/*.adoc
Positive Lookbehind (?⇐…​)

Match X only if preceded by Y:

# Match "quantum" only when preceded by "tmux-"
grep -P '(?<=tmux-)quantum' sessions/*.yml

# Match filenames after "include::partial$"
grep -oP '(?<=include::partial\$)[\w/.-]+' docs/modules/ROOT/pages/**/*.adoc
Negative Lookbehind (?<!…​)

Match X only if NOT preceded by Y:

# Match "quantum" NOT preceded by "dots-" — finds tmux-quantum but not dots-quantum
grep -P '(?<!dots-)quantum' sessions/*.yml

# Match "ops" NOT preceded by "infra-" — finds siem-ops, linux-ops, etc.
grep -rP '(?<!infra-)ops' docs/
Lookbehind Limitations

Lookbehinds must be fixed length in most PCRE engines:

# WORKS — fixed length lookbehind
grep -P '(?<=domus-)captures' file

# FAILS — variable length lookbehind
grep -P '(?<=domus-\w+-)captures' file

# WORKAROUND — use \K instead (see below)
grep -P 'domus-\w+-\K captures' file

\K — Reset Match Start (PCRE Exclusive)

\K says "forget everything matched so far." Combined with -o, it extracts just the part after \K:

# Extract session names from YAML
grep -oP 'name:\s*\K\S+' sessions/*.yml
# Output: DAILY-FLEETING, PROJ-DOMUS-CAPTURES, CFG-DOTFILES ...

# Extract root paths
grep -oP 'root:\s*\K.+' sessions/*.yml

# Extract include targets (everything after "include::")
grep -oP 'include::\K[^\[]+' docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc
# Output: partial$worklog/urgent.adoc, partial$worklog/morning.adoc ...

\K is a variable-length lookbehind that actually works. Whenever (?⇐…​) fails because the lookbehind is variable length, use \K instead.

Word Boundaries \b

\b matches the boundary between a word character (\w = [a-zA-Z0-9_]) and a non-word character:

# Match "sed" the command, not "used" or "sedx"
grep -P '\bsed\b' file.adoc

# Match "awk" as a whole word
grep -P '\bawk\b' file.adoc

# Combine with lookahead — "sed" followed by space and "-i"
grep -P '\bsed\b\s+-i' scripts/*.sh

Without \b, grep 'sed' matches "used", "sediment", "sedx". Word boundaries are cheap precision.

Character Classes and Shorthand

PCRE Equivalent Meaning

\d

[0-9]

Digit

\D

[^0-9]

Non-digit

\w

[a-zA-Z0-9_]

Word character

\W

[^a-zA-Z0-9_]

Non-word character

\s

[ \t\n\r\f]

Whitespace

\S

[^ \t\n\r\f]

Non-whitespace

.

(any)

Any character except newline

\d, \w, \s require -P (PCRE). ERE (-E) does not support them — use the bracket equivalents.
# Find date patterns (YYYY-MM-DD) in filenames
find docs -name '*.adoc' | grep -P '\d{4}-\d{2}-\d{2}'

# Find lines with IP addresses (rough match)
grep -P '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b' file.adoc

# Find port numbers after a colon
grep -oP ':\K\d{2,5}\b' file.adoc

Quantifiers

Quantifier Meaning

*

Zero or more (greedy)

+

One or more (greedy)

?

Zero or one (optional)

{n}

Exactly n

{n,}

n or more

{n,m}

Between n and m

*?, +?, {n,m}?

Non-greedy (lazy) versions — match as little as possible

# Greedy — matches longest possible
echo 'include::partial$worklog/daily-notes/2026-06/2026-06-24.adoc[]' | grep -oP 'partial\$.+'
# Output: partial$worklog/daily-notes/2026-06/2026-06-24.adoc[]

# Non-greedy — matches shortest possible
echo 'include::partial$worklog/daily-notes/2026-06/2026-06-24.adoc[]' | grep -oP 'partial\$.+?/'
# Output: partial$worklog/

Capture Groups and Backreferences

# Capture group — match repeated words (typo detection)
grep -P '\b(\w+)\s+\1\b' docs/**/*.adoc
# Catches: "the the", "is is", "to to"

# Named capture group (informational — grep doesn't extract by name, but useful in scripts)
grep -P '(?P<date>\d{4}-\d{2}-\d{2})' file.adoc

# Alternation inside groups
grep -P '(tmux|dots)-quantum' sessions/*.yml

Anchors

Anchor Meaning

^

Start of line

$

End of line

\A

Start of string (PCRE — ignores multiline)

\Z

End of string (PCRE)

# Lines starting with "include::"
grep -P '^include::' docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc

# Lines ending with ".adoc[]"
grep -P '\.adoc\[\]$' docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc

# Lines that are ONLY an include directive (nothing else)
grep -P '^include::\S+\[\]$' docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc

Real-World Composition Patterns

Extract all include targets from a worklog
grep -oP '(?<=include::)\S+(?=\[)' docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc

Positive lookbehind (?⇐include::) anchors after the directive. Positive lookahead (?=\[) stops before the [. Only the path is matched.

Find sessions with more than 5 windows
for f in sessions/*.yml; do
  count=$(grep -cP '^\s+- \w+' "$f")
  [ "$count" -gt 5 ] && printf "%-30s %d windows\n" "$(basename $f)" "$count"
done
Find AsciiDoc attribute references that might be undefined
# Extract all {attribute} references from a file
grep -oP '\{[\w-]+\}' docs/modules/ROOT/pages/**/*.adoc | sort -u

# Cross-reference against defined attributes
comm -23 \
  <(grep -rhoP '\{[\w-]+\}' docs/modules/ROOT/pages/ | sort -u) \
  <(grep -oP '^\s*:[\w-]+:' docs/antora.yml | sed 's/.*:\(.*\):/{\1}/' | sort -u)
Find files where grep -E would miss but -P catches
# ERE can't do this — \d requires PCRE
grep -P 'WRKLOG-\d{4}-\d{2}-\d{2}' docs/modules/ROOT/pages/2026/06/*.adoc

# ERE equivalent (verbose)
grep -E 'WRKLOG-[0-9]{4}-[0-9]{2}-[0-9]{2}' docs/modules/ROOT/pages/2026/06/*.adoc

Decision Tree: Which grep Mode?

Need alternation (a|b)?
  └─ Yes → grep -E (ERE is sufficient)

Need \d, \w, \b shorthand?
  └─ Yes → grep -P (PCRE)

Need lookaheads (?=) or lookbehinds (?<=)?
  └─ Yes → grep -P (PCRE only)

Need \K to extract values?
  └─ Yes → grep -oP (PCRE + only-matching)

Simple fixed string?
  └─ Yes → grep -F (fastest — no regex engine)

The Lesson

The 2026-06-24 debugging session failed not because of regex syntax but because of working directory. The regex 'tmux-quantum|sed-append-cli' with -E was correct ERE alternation. But grep -rn searched docs/ relative to tmux-quantum/, not domus-captures/.

Always verify where before debugging what.

Two ways to make this error impossible:

# 1. Absolute paths — works from anywhere
grep -rn -E 'pattern' ~/atelier/_bibliotheca/domus-captures/docs/

# 2. Check your working directory first
pwd && grep -rn -E 'pattern' docs/

CLI Pattern: sed Append After Pattern Match

Appending a line to a file without opening an editor — sed pattern-addressed a\ command.

The Problem

Insert an include:: directive into an AsciiDoc file after a known anchor line, from the terminal.

sed — Pattern-Addressed Append

# Pattern-addressed — append after the line matching the regex
sed -i '/pattern/a\new line of text' file.adoc

/regex/a\text — when sed matches the regex, it appends text on the next line. -i edits in place (no temp file needed).

Line-addressed variant (brittle — breaks if line numbers shift):

sed -i '19a\new line here' file.adoc

Pattern-addressed variant (survives line drift):

sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc[]' \
  ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-24.adoc

awk — When You Need More Control

awk '
\{print}
/pattern/{
  print "new line of text"
}' file.adoc > /tmp/out.tmp && mv /tmp/out.tmp file.adoc

Affordance difference: sed -i edits in place natively. awk has no -i — redirect to temp file, then mv back. But awk gives you state machines, multi-line logic, and conditionals that sed’s hold buffer makes painful. For a single-line append, sed is the right tool.

Verify Pattern (Before / Change / After)

# Before
awk 'NR>=17 && NR<=21' file.adoc

# Change
sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc[]' file.adoc

# After
awk 'NR>=17 && NR<=22' file.adoc

Idempotent Version (For Scripts / Future Use)

# Only add if not already present — grep -q guard
grep -q 'tmux-quantum-session-restructure' "$file" || \
  sed -i '/_Day-specific accomplishments here/a\include::partial$worklog/daily-notes/2026-06/2026-06-24/tmux-quantum-session-restructure.adoc[]' "$file"

grep -q exits silently with 0 (found) or 1 (not found). The || short-circuit means sed only runs when the line is absent. Run it ten times, same result.

Quick Reference

Operation sed awk

Append after pattern

sed -i '/pat/a\text' f

awk '{print} /pat/{print "text"}' f > tmp && mv tmp f

Insert before pattern

sed -i '/pat/i\text' f

awk '/pat/{print "text"} {print}' f > tmp && mv tmp f

Replace line

sed -i '/pat/c\text' f

awk '/pat/{$0="text"} {print}' f > tmp && mv tmp f

Delete line

sed -i '/pat/d' f

awk '!/pat/' f > tmp && mv tmp f

Restructured tmux-quantum sessions directory — data-driven audit of all 25 tmuxinator YAMLs against actual git commit frequency and system utilization. Reduced to 18 active sessions (8 new, 3 merged, 16 archived).

tmux-quantum Session Restructure

Context

The sessions/ directory in tmux-quantum had 25 flat YAML files. Half were skeletons with empty panes and placeholder comments. Several pointed to repos with zero recent commits. Four actively-used repos (40+ commits/quarter) had no sessions at all.

Methodology

Audited every repo under ~/atelier/ using git commit frequency (90-day window), file modification timestamps, running tmux sessions, and shell history for tmuxinator start invocations.

Key findings:

Repo 90d Commits Had Session?

domus-captures

1,901

Yes (×2)

dots-quantum

115

Yes

domus-asciidoc-build

40

No

domus-docs

37

No

domus-nvim

25

Yes (×3)

netapi

16

No

domus-infra-ops

14

No

domus-api

13

No

association-engine

8

No

crypta

4

No

13 sessions pointed to repos with zero 90-day commits (Principia paths, obsolete predecessors, inactive projects).

Changes

Created 8 new sessions for repos that lacked them:

  • proj-domus-build — domus-asciidoc-build (converters, themes, templates)

  • proj-netapi — netapi CLI (ISE ERS, DataConnect, reports)

  • proj-domus-infra — domus-infra-ops (infrastructure docs spoke)

  • proj-domus-api — domus-api (FastAPI content management)

  • proj-assoc-engine — association-engine (knowledge graph)

  • proj-domus-docs — Antora aggregator hub

  • proj-domus-spokes — multi-spoke workspace (7 spoke repos in one session)

  • proj-crypta — Rust educational roguelike

Merged 3 → 1:

  • cfg-nvim-lsp + train-nvim + test-nvim-pluginscfg-nvim

  • Three sessions targeting the same repo (domus-nvim) consolidated into one with windows for config, plugins, LSP, telescope, testing

Rewrote 3 sessions:

  • proj-domus-captures — added worklog, drafts, scripts windows matching actual daily workflow; fixed antora window (was opening aggregator playbook instead of component descriptor)

  • daily-fleeting — removed stale npx antora build references, cleaned up

  • cfg-tmux — repointed from generic ~/.config/tmux to tmux-quantum repo with proper module/theme/script windows; fixed tmux source-file ~/.tmux.conf~/.config/tmux/tmux.conf

Fixed 2 sessions:

  • proj-netapi-tui — removed venv/bin/activate references, switched to uv

  • lrn-spanish — removed blocking read -p, added edicion-analitica window for quijote analytical edition

Archived 16 sessions to _archive/:

All zero-activity sessions: cfg-ansible, cfg-email, cfg-oci-vm, proj-tmux-config (obsolete predecessor), proj-aethelred, proj-work (empty dir), data-analysis, lab-vimiff, train-diffview, train-pyats-report-gen, train-python, train-ripgrep, train-sql-py-json, plus the 3 merged originals.

Added INDEX.adoc — quick reference with naming conventions and session descriptions.

Result

sessions/
├── 18 active .yml files     (all YAML-validated)
├── _archive/                (16 archived)
└── INDEX.adoc               (quick reference)

Validation

# Syntax
for f in sessions/*.yml; do yq '.' "$f" > /dev/null 2>&1 && echo "OK $(basename $f)"; done

# Root paths exist
for f in sessions/*.yml; do
  root=$(yq -r '.root' "$f" | sed "s|~|$HOME|")
  [ ! -d "$root" ] && echo "BAD: $(basename $f) → $root"
done

# Name convention (UPPERCASE)
yq -r '.name' sessions/*.yml | awk '$0 != toupper($0) {print "LOWERCASE: " $0}'

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.

Search daily notes — find commands, builds, and patterns across worklogs

Source: 2026-06-22 — searching for build-antora-page invocations and theme usage across partials

which daily note partials mention a command?
# Find files — filenames only
grep -rl 'build-antora-page' docs/modules/ROOT/partials/worklog/
see the match in context without opening the file
# 3 lines of context around each hit
grep -rn -C3 'build-antora-page' docs/modules/ROOT/partials/worklog/
search for any theme flag across all partials (PCRE alternation)
grep -rPn 'theme\s+(light-cyan|catppuccin|mocha)' docs/modules/ROOT/partials/
find + grep — search all daily note partials for build/export patterns
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec grep -l 'build-antora-page\|build-adoc\|--theme\|pdf' {} \;
find + awk — show filename:matching_line for every hit (no -r needed)
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec awk '/build-antora-page|build-adoc|--theme/{print FILENAME": "$0}' {} \;
awk state machine — extract full [source,bash] blocks containing a command
find docs/modules/ROOT/partials -name '*.adoc' -exec \
  awk '/^\[source,bash\]/{block=1; buf=""} \
       block{buf=buf"\n"$0} \
       /^----$/ && block>1{if(buf~/build-antora-page/) print FILENAME":"buf; block=0} \
       block{block++}' {} \;

The awk state machine: [source,bash] sets block=1 and starts buffering. Each line appends to buf. When the closing ---- arrives (and block>1 to skip the opening fence), check if the buffer contains the target command. If yes, print filename + entire block. Reset. One pass, arbitrary block size, no temp files.

sed address range — print only lines between code fences matching a pattern
find docs/modules/ROOT/partials/worklog -name '*.adoc' -exec \
  sed -n '/\[source,bash\]/,/^----$/{/build-antora-page/p}' {} +
search data/ too — standalone adoc outside Antora
grep -rn 'build-antora-page\|build-adoc' data/ --include='*.adoc'
every invocation sorted by path (chronological by date directory)
grep -rn 'build-antora-page' docs/modules/ROOT/partials/worklog/daily-notes/ \
  | awk -F: '{print $1, $3}' | sort

Pattern escalation: grep -rl → "where is it". grep -rn -C3 → "what’s around it". find -exec awk → "extract the structured block". sed address ranges → "print between delimiters". Each tool has a different affordance — grep finds, awk extracts structure, sed filters ranges, find -exec scales across trees.

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.

pacman — package inspection

Source: 2026-06-25 — curl error 77 investigation, checking for package upgrades

# Check package version and install date (awk field filter)
pacman -Qi curl | awk '/^Version|^Install Date/'