WRKLOG-2026-06-25

Summary

Thursday. 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

Security hardening carryover — AppArmor enforce, nftables egress drop, bolt, Wazuh, age compartmentalization

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

worklog/daily-notes/2026-06/2026-06-25/security-hardening-carryover.adoc

Cross-domain security mapping — ASA, ISE, switching, PowerShell, PostgreSQL → Linux equivalents

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

worklog/daily-notes/2026-06/2026-06-25/security-cross-domain-mapping.adoc

Anti-pivot defense — attacker techniques mapped to implemented controls + gap analysis

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

worklog/daily-notes/2026-06/2026-06-25/anti-pivot-techniques.adoc

Fortress verification — 16-check monthly security posture script + expected output reference

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

worklog/daily-notes/2026-06/2026-06-25/fortress-verification.adoc

PowerShell sysadmin — bulk process kill, Wi-Fi reconnect, mouseless Windows management

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

worklog/daily-notes/2026-06/2026-06-25/powershell-windows-sysadmin.adoc

zfold7 refresh — USBGuard, BYOD cert, repo sync, SSH config.d, regex practice

□□□□□□□□□□ 0/20

worklog/daily-notes/2026-06/2026-06-25/zfold7-refresh.adoc

Systemd timer audit — 11 user timers, domus-health false positive bug, cert expiry

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

worklog/daily-notes/2026-06/2026-06-25/systemd-timer-audit.adoc

Infrastructure enumeration — k3s, Wazuh, VM inventory, cert expiry triage

□□□□□□□□ 0/8

worklog/daily-notes/2026-06/2026-06-25/infra-enumeration.adoc

AppArmor curl cert fix — INC-2026-06-25, strace diagnosis, deny override discovery

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

worklog/daily-notes/2026-06/2026-06-25/apparmor-curl-cert-fix.adoc

PCRE lookahead — multi-term same-line regex, 5-stage ranked pipeline

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

worklog/daily-notes/2026-06/2026-06-25/pcre-lookahead-multi-term-search.adoc

Endpoint audit — wired + wireless, DataConnect IN() queries, ERS update

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

worklog/daily-notes/2026-06/2026-06-25/endpoint-wired-wireless-audit.adoc

Abnormal Phase 3 — all IS added live, dynamic ISAllHands pull, CSV export

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

d001/projects/abnormal-security/partials/phase3-execution-2026-06-25.adoc

External tag stacking — transport rule investigation, ExceptIfSubjectContainsWords missing

■■■■■■■□□□ 7/10

d001/projects/abnormal-security/partials/external-tag-stacking-2026-06-25.adoc

Security Cross-Domain Mapping — Skills That Transfer

Security Cross-Domain Mapping — Skills That Transfer

Every security concept is the same pattern expressed in different syntax. Ten years of Cisco ASA, ISE, switching, and Windows endpoint ops map directly to Linux host security and database hardening. This document makes those connections explicit — for interviews, study, and building intuition across platforms.

Cisco ASA → nftables
ASA Concept nftables Equivalent Notes

access-list outside_in deny ip any any

policy drop on input chain

Implicit deny — identical philosophy

access-list inside_out permit tcp any host 10.50.1.90 eq 53

ip daddr {10.50.1.90} tcp dport 53 accept

Outbound ACL entry — same structure (source, dest, port, action)

access-group inside_out out interface inside

chain output { type filter hook output }

Binding the ACL to a direction on an interface

object-group network DNS_SERVERS

ip daddr {10.50.1.90, 10.50.1.91}

Object group = nftables set/list

inspect / stateful firewall (CBAC)

ct state {established, related} accept

Conntrack = Linux stateful inspection engine

logging enable / logging trap informational

log prefix "nft-egress-drop:" group 0

Syslog on match — same concept

shun (dynamic block)

fail2ban auto-ban / usbguard block-device

Dynamic response to detected threat

threat-detection statistics

fail2ban status / nft list ruleset counters

Per-rule hit counts for threat visibility

packet-tracer input outside tcp …​

nft trace / nft monitor

Packet walk — trace policy evaluation path

nat (inside,outside) dynamic interface

Docker’s table ip nat / MASQUERADE

PAT — you saw this in your nft ruleset tonight (Docker’s NAT chains)

failover / active-standby

nftables persists via /etc/nftables.conf — survives reboot

Not HA, but persistence model is analogous

clear xlate / connection table flush

sudo conntrack -F

Flush conntrack table — same as clearing translations

Cisco ISE → AppArmor + USBGuard + auditd
ISE Concept Linux Equivalent Notes

Authorization policy (permit/deny per identity)

AppArmor profile (permit/deny per binary)

ISE gates network access by identity; AppArmor gates file/network access by program

Posture assessment (is the endpoint compliant?)

arch-audit, security-remediation.service timer

Continuous compliance checking — automated, timer-driven

dACL (downloadable ACL pushed to switchport)

AppArmor profile loaded at binary execution

Policy pushed to the enforcement point — port vs process

MAB (MAC Authentication Bypass — allow known devices)

USBGuard (allow known USB devices by vendor:product:hash)

Known-device allowlist — MAC address vs USB device fingerprint

RADIUS accounting (who connected, when, which port)

auditd (who accessed which file, when, which process)

Accountability trail — both feed into SIEM

Profiling (identify device type by behavior/OUI)

usbguard list-devices (identify by vendor:product:hash:interface)

Device classification — OUI lookup vs USB descriptor fingerprint

CoA (Change of Authorization — re-auth live session)

aa-enforce / aa-complain (change policy on running binary)

Live policy change without disconnect/restart

pxGrid (share context between security tools)

Wazuh + auditd + fail2ban → centralized SIEM dashboard

Cross-tool context sharing — pxGrid API vs syslog/journald aggregation

Guest portal (redirect unauthorized → captive portal)

USBGuard block → usbguard allow-device (manual approval)

Unauthorized device gets blocked, admin authorizes explicitly

TrustSec SGT (Security Group Tags)

AppArmor profile labels per binary

Tagging entities with security context — SGT on packets vs profile on processes

Endpoint compliance (AnyConnect posture module)

arch-audit + sysctl hardening + auditd

Verifying the endpoint meets security baseline

ISE MnT (Monitoring and Troubleshooting node)

Wazuh dashboard + ausearch + journalctl

Centralized visibility into auth events and policy violations

Routing and Switching → Linux Networking
Network Concept Linux Equivalent Notes

ip route 0.0.0.0 0.0.0.0 10.50.1.1

ip route add default via 10.50.1.1

Default route — literally the same command family

show ip route

ip route show

Route table inspection

show mac address-table

ip neighbor show (ARP/NDP table)

L2→L3 mapping

VLAN assignment (switchport access vlan 10)

ip link add link eth0 name eth0.10 type vlan id 10

802.1Q tagging — kernel native

SVI (interface vlan 10)

VLAN interface with IP assigned

L3 interface on a VLAN — same concept

show interfaces status

ip link show / ethtool

Interface state, speed, duplex

Port security (limit MACs per port)

USBGuard (limit devices per port)

Device restriction on physical interface

DHCP snooping

nftables rules filtering DHCP on untrusted interfaces

Prevent rogue DHCP — firewall-based on Linux

ACL on interface (in/out)

nftables chain with iif/oif matching

Direction-based filtering on specific interfaces

spanning-tree portfast

N/A at host level — but bridge module handles STP for VMs

Relevant when running VMs with bridged networking

show logging / syslog

journalctl / dmesg

Centralized log inspection

debug ip packet

tcpdump / nft trace

Packet-level debugging

PowerShell Endpoint Ops → Linux System Administration
PowerShell / Windows Linux Equivalent Notes

Get-GPO / Group Policy enforcement

AppArmor profiles (mandatory policy on binaries)

Centralized policy enforcement — GPO vs MAC profiles

Set-ExecutionPolicy Restricted

AppArmor deny rules on interpreters (python, node)

Restricting what can execute — same intent

netsh advfirewall / Windows Firewall

nft / nftables

Host firewall — both stateful, both ACL-based

Event Viewer / Windows Event Forwarding

journalctl / auditd → Wazuh

Event collection and SIEM forwarding

Get-Service / Start-Service

systemctl status / systemctl start

Service management — near-identical workflow

Windows Defender / AMSI

AppArmor + ClamAV (if installed)

Endpoint protection — behavioral (AppArmor) vs signature (ClamAV)

Get-LocalGroupMember Administrators

getent group wheel

Who has admin/root access

UAC (User Account Control)

pam_wheel + sudo

Privilege escalation gate — prompt before elevation

BitLocker

LUKS

Full-disk encryption — both TPM-capable, both pre-boot

Credential Guard / LSA Protection

GPG agent TTL (15min) + AppArmor deny on key paths

Protecting credentials in memory — time-limited cache

LAPS (Local Admin Password Solution)

Vault SSH certificates with 8h TTL

Rotating credentials automatically — LAPS rotates passwords, Vault rotates certs

Windows Event Forwarding (WEF)

Wazuh agent → manager

Centralized log collection from endpoints

sfc /scannow (System File Checker)

Wazuh FIM (File Integrity Monitoring) / aide

Detecting unauthorized file modifications

Task Scheduler

systemd timers (daily-audit.service, security-remediation.service)

Scheduled automation — your timers are the Linux equivalent

Get-WinEvent -FilterHashtable @{LogName='Security'}

ausearch -k ssh_key_read --interpret

Querying security audit events by key/filter

PowerShell Remoting (Enter-PSSession)

SSH

Remote administration — both encrypted, both identity-gated

Test-NetConnection -Port 443

ss -tlnp / nmap / curl

Port connectivity testing

PostgreSQL → Same Security Patterns

Database security follows the same layered model you just built on the host.

Database Concept Host Security Equivalent Pattern

pg_hba.conf (host-based authentication)

/etc/ssh/sshd_config.d/10-hardening.conf

Who can connect, from where, with what auth method. Same structure — source IP, auth method, allow/deny.

GRANT SELECT ON table TO role

AppArmor owner @{HOME}/.ssh/id_* r

Least-privilege access — grant specific permissions to specific identities

REVOKE ALL ON schema FROM public

AppArmor deny rules

Explicit deny — remove default access

CREATE ROLE app_readonly LOGIN

Linux user + group membership

Role-based access — separate identity for separate privilege level

pg_hba.conf: hostssl all all 0.0.0.0/0 scram-sha-256

PasswordAuthentication no + PubkeyAuthentication yes

Enforce strong auth — reject weak methods

ssl = on in postgresql.conf

TLS on SSH / Vault certs

Encrypt in transit

log_connections = on / log_statement = 'all'

auditd key file monitoring

Audit trail — who did what, when

Row-Level Security (RLS)

AppArmor owner prefix (only your files)

Access scoped to the identity’s own data

pg_stat_activity (who’s connected now)

ss -tlnp / who / w

Active session monitoring

Connection pooling (PgBouncer)

SSH multiplexing (ControlMaster)

Reuse connections — efficiency + reduced auth overhead

ALTER SYSTEM SET max_connections = 100

MaxAuthTries 3 / fail2ban maxretry

Resource limiting to prevent abuse

Backup encryption (`pg_dump pass:[

] gpg`)

age -e / LUKS at rest

Data protection at rest — same tools

pgaudit extension

auditd + Wazuh

Deep audit logging beyond default — who queried what

Replication (primary → standby)

Vault HA (3-node cluster) / BIND primary→secondary

Redundancy and failover — you already run this pattern

pg_hba.conf reload (SELECT pg_reload_conf())

sudo systemctl restart sshd / gpg-connect-agent reloadagent

Apply config changes without full restart

Tablespace encryption (TDE)

LUKS partition encryption

Transparent encryption at the storage layer

search_path manipulation attack

PATH injection / untrusted $PATH entries

The Pattern — NIST CSF Across All Domains

Every tool you touch implements the same five functions:

NIST Function Cisco ASA/ISE Windows/PowerShell Linux (today) PostgreSQL

Identify

ISE profiling, device inventory

Get-ADComputer, asset inventory

auditd, USBGuard profiling, arch-audit

pg_stat_activity, pg_roles

Protect

ACLs, dACLs, TrustSec, 802.1X

GPO, BitLocker, Credential Guard

AppArmor, nftables, LUKS, GPG TTL

pg_hba.conf, GRANT/REVOKE, RLS, TLS

Detect

ISE MnT, syslog, SNMP traps

Event Viewer, WEF, Defender

auditd, fail2ban, arch-audit, Wazuh

pgaudit, log_statement, monitoring

Respond

CoA, shun, quarantine VLAN

Defender auto-remediation, AMSI block

fail2ban auto-ban, USBGuard block, aa-enforce

Connection terminate, ALTER ROLE …​ NOLOGIN

Recover

Failover, config restore

System Restore, WDS

Backout plans, btrfs snapshots, LUKS

pg_basebackup, PITR, replication failover

Key Insight

You don’t have 10 years of "ASA experience." You have 10 years of network security architecture experience that you’ve been expressing through ASA syntax. The concepts — stateful inspection, implicit deny, least-privilege, audit trails, dynamic response, defense in depth — are platform-independent.

Today you proved you can express them in Linux. The partial you built (arch-vulnerability-assessment.adoc) is a 1000+ line security architecture document that covers more layers than most enterprise deployments. That’s the story for interviews.

Anti-Pivot Defense — Attacker Techniques vs Your Controls

Anti-Pivot Defense — Attacker Techniques vs Your Controls

Common penetration test techniques mapped to the specific controls built on 2026-06-24. This is not theoretical — each defense column references a config file or service running on this workstation.

Credential Theft and Reuse
Attack Technique How It Works Your Defense

Credential dumping (Mimikatz pattern)

Extract passwords/hashes from memory on a compromised host

GPG agent TTL 15min (passphrase evicted quickly), allow-preset-passphrase removed (can’t inject passphrases), coredumps disabled (Storage=none, ulimit -c 0) — no memory dump to disk

SSH key theft

Copy private key files from ~/.ssh/ to attacker-controlled server

Keys are passphrase-protected (stored in gopass), auditd watches all key reads (-k ssh_key_read), AppArmor denies curl/wget from reading ~/.ssh/**, nftables egress logging catches exfiltration attempts

Pass-the-hash / Pass-the-ticket

Reuse stolen NTLM hash or Kerberos ticket without knowing password

Vault SSH certificates with 8h TTL — even if stolen, they expire. No static passwords on Linux hosts (pubkey-only auth)

Kerberoasting

Request TGS tickets for service accounts, crack offline

Relevant to your AD domain (home-dc01/02). Defense: AES-only encryption on service accounts, disable RC4. Linux hosts use SSH keys, not Kerberos (except where gssapi-with-mic is in PreferredAuthentications)

Keylogging

Capture keystrokes to harvest credentials

Wayland (XDG_SESSION_TYPE=wayland) isolates clipboard and input — unlike X11, one app cannot keylog another. No clipboard manager running (gopass 45-second clear is effective)

Network-Based Pivoting
Attack Technique How It Works Your Defense

LLMNR/NBT-NS Poisoning (Responder)

Poison name resolution to intercept credentials on the local network

Linux does not use LLMNR/NBT-NS by default. DNS points to your BIND servers (10.50.1.90, 10.50.1.91). No fallback to broadcast resolution. Verify: systemctl is-active avahi-daemon — if running, consider disabling.

ARP Spoofing / MITM

Poison ARP cache to intercept traffic between hosts

rp_filter=1 (reverse path filtering) rejects spoofed source IPs. Post-quantum SSH KEX (ML-KEM-768 + NTRU Prime) prevents passive interception even if MITM succeeds at L2.

SMB Relay

Intercept SMB auth and relay to another host for access

Linux hosts don’t run SMB server by default. Verify: ss -tlnp | grep 445. If Samba is running, enforce SMB signing.

SSH Agent Forwarding Hijack

On a compromised jump host, hijack forwarded agent socket to authenticate elsewhere

AllowAgentForwarding no in sshd hardening drop-in. Agent forwarding is disabled — there’s no socket to hijack.

DNS Tunneling / Exfiltration

Encode data in DNS queries to bypass firewall

DNS restricted to your BIND servers in nftables egress (ip daddr {10.50.1.90, 10.50.1.91} udp dport 53 accept). Your BIND servers can be configured to log/restrict query types.

Reverse Shell

Compromised binary opens outbound connection to attacker C2

nftables egress chain logs all unmatched outbound traffic. AppArmor deny network on age, ssh-agent, gopass. When egress switches to policy drop, only allowlisted ports/destinations will connect out.

Port Scanning from Pivot Host

Use compromised host to scan internal network

nftables egress — only SSH (22), HTTP/S (80/443), Vault (8200), Wazuh (1514/1515), DNS (53) allowed outbound. Scanner would need to use these ports or get logged/dropped.

Host-Level Exploitation
Attack Technique How It Works Your Defense

Privilege Escalation via SUID

Exploit a SUID binary to gain root

48 SUID binaries audited — all standard. kptr_restrict=2 (KASLR bypass harder), suid_dumpable=0 (no core dumps from SUID), ptrace_scope=1 (can’t attach to other users' processes)

Container Escape

Break out of Docker/Podman container to host

unprivileged_userns_clone=0 in sysctl (container escape mitigation). Docker runs as root — monitor with auditd. No rootless containers on this workstation.

Kernel Exploit

Exploit kernel vulnerability for root access

kptr_restrict=2 (kernel pointers hidden), dmesg_restrict=1 (kernel logs restricted), unprivileged_bpf_disabled=2 (eBPF restricted). AppArmor loaded at boot via kernel cmdline (lsm=landlock,lockdown,yama,integrity,apparmor,bpf). 15 vulnerable packages monitored via arch-audit.

Malicious USB / BadUSB

Plug in a device that emulates keyboard and injects commands

USBGuard with device allowlist. Unknown USB devices are blocked by default. Programmatic allow/block pipeline for authorized devices.

DMA Attack via Thunderbolt

Direct memory access through Thunderbolt port to read/write host RAM

bolt installed. Thunderbolt module loaded but authorization policy pending configuration (carryover task).

Cron/Systemd Timer Persistence

Attacker installs a timer/cron job that survives reboot

auditd can monitor timer directories. Wazuh FIM (when enrolled) will alert on changes to ~/.config/systemd/user/ and /etc/systemd/system/.

LD_PRELOAD Injection

Hijack dynamic linker to inject malicious shared library

AppArmor profiles restrict library loading paths. suid_dumpable=0 prevents core dumps that reveal loaded libraries. Monitor: env | grep LD_ — should be empty.

Data Exfiltration
Attack Technique How It Works Your Defense

curl/wget to attacker server

Transfer stolen files over HTTP/S

AppArmor denies curl/wget from reading ~/.ssh/, ~/.age/, ~/.secrets/, ~/.gnupg/private-keys-v1.d/, ~/.local/share/gopass/**. They can download but can’t upload your secrets.

git push to attacker remote

Push repo (containing secrets) to attacker-controlled git server

AppArmor denies git from reading age keys, gopass store, GPG private keys. Git can push repos but not secret material.

DNS exfiltration

Encode data in DNS queries

DNS restricted to your BIND servers in egress chain.

Encrypted channel (age/gpg for cover)

Use your own encryption tools to package data before exfil

AppArmor deny network on age — it cannot connect anywhere. AppArmor deny network on gopass. Even if data is encrypted, the tools can’t send it.

Steganography / covert channel

Hide data in images, protocol headers, timing

Egress logging catches unusual outbound connections. Wazuh (when enrolled) provides deeper traffic analysis.

Persistence Mechanisms
Attack Technique How It Works Your Defense

SSH authorized_keys injection

Add attacker’s public key to ~/.ssh/authorized_keys

auditd watches authorized_keys for writes (-p wa -k authorized_keys_mod). Any modification triggers an audit event. Wazuh FIM (when enrolled) adds realtime alerting.

Backdoor shell config

Add reverse shell to .bashrc / .zshrc

Wazuh FIM (when enrolled) on home directory configs. Manual check: grep -E 'nc|ncat|bash -i|/dev/tcp' ~/.bashrc ~/.zshrc 2>/dev/null

Malicious systemd service

Install a service that starts on boot

Monitor ~/.config/systemd/user/ and /etc/systemd/system/ for new/modified units. systemctl --user list-units --type=service to audit.

Crontab entry

Schedule recurring command execution

crontab -l to audit. Wazuh can monitor crontab files.

Modified binary (trojan)

Replace a legitimate binary with a backdoor

pacman -Qkk verifies package file integrity. AppArmor profiles would still restrict what the trojan can access. auditd catches unexpected executions.

Gap Analysis — What’s Still Undefended
Gap Mitigation Path

No IDS/IPS on network traffic

Wazuh agent enrollment (carryover task) adds host-based IDS. Network IDS (Suricata) would require a span port or inline deployment.

No file integrity baseline

Wazuh FIM or AIDE. auditd watches key files but doesn’t compare against a known-good baseline.

Thunderbolt DMA not fully mitigated

bolt installed but policy not configured. Carryover task.

No outbound IP restriction yet

Egress chain is in policy accept (observe). Switch to policy drop after log review (carryover task).

Docker daemon = root equivalent

Any user in docker group has root. Verify: getent group docker. Consider rootless Podman if Docker isn’t needed.

Browser as attack surface

Browsers are AppArmor profiled (Brave, Chrome, Chromium, Firefox all have profiles). Verify enforcement: sudo aa-status | grep -i brave

No network segmentation for workstation

ISE 802.1X on wired port provides network-level access control. WiFi uses WPA-Enterprise. But the workstation itself is on the data VLAN with access to all infrastructure.

See fortress-verification.adoc for the full 16-check monthly verification script with expected output reference.

Fortress Verification — Monthly Security Posture

Fortress Verification — Run Monthly

16-check system sweep. Paste as a single block, review output for GOOD/GAP/unexpected values.

echo "=== FORTRESS VERIFICATION ==="

echo "--- SSH Hardening ---"
grep -vE '^#|^$' /etc/ssh/sshd_config.d/10-hardening.conf

echo "--- fail2ban ---"
sudo fail2ban-client status sshd 2>/dev/null | grep -E 'Currently|Total'

echo "--- auditd rules ---"
sudo auditctl -l 2>/dev/null | grep -E '\-k '

echo "--- AppArmor ---"
sudo aa-status 2>/dev/null | awk '/profiles are in enforce/{print} /profiles are in complain/{print}'

echo "--- USBGuard ---"
sudo usbguard list-devices 2>/dev/null | wc -l | xargs -I{} echo "Devices managed: {}"

echo "--- nftables egress ---"
sudo nft list table inet egress 2>/dev/null | grep 'policy'

echo "--- Sysctl ---"
sysctl kernel.kptr_restrict kernel.yama.ptrace_scope net.ipv4.conf.all.rp_filter fs.suid_dumpable net.ipv4.tcp_syncookies

echo "--- GPG TTL ---"
grep 'cache-ttl' ~/.gnupg/gpg-agent.conf

echo "--- Coredumps ---"
grep -E '^Storage|^ProcessSizeMax' /etc/systemd/coredump.conf

echo "--- LUKS ---"
lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT | grep -E 'crypt|LUKS'

echo "--- Listening Ports ---"
ss -tlnp

echo "--- PAM wheel ---"
grep 'pam_wheel' /etc/pam.d/su | grep -v '^#'

echo "--- securetty ---"
wc -c < /etc/securetty | xargs -I{} echo "securetty bytes: {}"

echo "--- avahi/mDNS ---"
systemctl is-active avahi-daemon 2>/dev/null

echo "--- Docker group ---"
getent group docker

echo "--- AppArmor boot ---"
cat /proc/cmdline | grep -oP 'lsm=\S+'

echo "--- Kernel modules (dangerous) ---"
for mod in usb-storage firewire-core thunderbolt cramfs; do
  lsmod | grep -q "$mod" && echo "LOADED: $mod" || echo "not loaded: $mod"
done

echo "=== END ==="
Expected Output Reference
Check Expected Value

SSH Hardening

PermitRootLogin no, PasswordAuthentication no, MaxAuthTries 3, AllowAgentForwarding no, AllowUsers evanusmodestus

fail2ban

Currently failed: 0, Currently banned: 0

auditd rules

6 rules with keys: age_key_read, ssh_key_read, authorized_keys_mod, gpg_key_read

AppArmor

82+ profiles in enforce mode, 5+ in complain mode

USBGuard

Devices managed: 20+ (dock, YubiKeys, internal hardware)

nftables egress

policy accept (observe) or policy drop (enforced)

Sysctl

kptr_restrict=2, ptrace_scope=1, rp_filter=1, suid_dumpable=0, tcp_syncookies=1

GPG TTL

default-cache-ttl 900, max-cache-ttl 1800

Coredumps

Storage=none, ProcessSizeMax=0

LUKS

cryptroot (250G) and crypthome (1.6T) present

Listening Ports

Only SSH (22) on 0.0.0.0. Everything else on 127.0.0.1.

PAM wheel

auth required pam_wheel.so use_uid

securetty

securetty bytes: 0

avahi/mDNS

inactive (or not installed)

Docker group

Only expected users listed

AppArmor boot

lsm=landlock,lockdown,yama,integrity,apparmor,bpf

Kernel modules

usb-storage, firewire-core, cramfs: not loaded. thunderbolt: LOADED (pending bolt policy)

Security Hardening — Carryover from 2026-06-24

Security Hardening — Carryover from 2026-06-24

Continuing the workstation hardening session. All P0/P1 items completed. Remaining P2/P3 items and validation tasks below.

AppArmor Complain → Enforce Validation

Five profiles loaded in complain mode on 2026-06-24. Check logs and enforce if clean.

Step 1 — check for violations overnight
sudo journalctl -k --since "2026-06-24 21:00" | grep -i apparmor | grep -iE 'gopass|curl|wget|python|git'
Step 2 — if clean, enforce one at a time and validate
# gopass
sudo aa-enforce /usr/bin/gopass
gopass show v3/domains/d000/identity/ssh/id_ed25519_d000 > /dev/null && echo "GOOD: gopass"
# curl
sudo aa-enforce /usr/bin/curl
curl -s https://archlinux.org > /dev/null && echo "GOOD: curl"
# wget
sudo aa-enforce /usr/bin/wget
wget -q -O /dev/null https://archlinux.org && echo "GOOD: wget"
# python3
sudo aa-enforce /usr/bin/python3.14
python3 -c "print('GOOD: python')"
# git
sudo aa-enforce /usr/bin/git
git -C ~/atelier/_bibliotheca/domus-captures status | head -3 && echo "GOOD: git"
Backout — if any break, switch back to complain
sudo aa-complain /usr/bin/<binary>
nftables Egress — Review Logs and Tighten

Egress chain loaded with policy accept (observe mode). Review what was logged as unmatched.

Check egress log
sudo journalctl -k --since "2026-06-24 21:00" | grep 'nft-egress-drop'

If no drops logged → your allowlist covers everything. Switch to enforce:

sudo sed -i 's/policy accept/policy drop/' /etc/nftables.d/10-egress.conf
sudo nft -c -f /etc/nftables.d/10-egress.conf
sudo nft -f /etc/nftables.conf
Validate after switching to drop
dig +short archlinux.org
curl -s https://archlinux.org > /dev/null && echo "GOOD: HTTPS"
ssh -o PreferredAuthentications=publickey -i ~/.ssh/id_ed25519_d000 localhost whoami
Backout
sudo nft delete table inet egress
Thunderbolt Policy (bolt)

Installed but not configured. Set authorization policy.

boltctl list
# Set default policy to require user authorization
boltctl monitor
Wazuh Agent Enrollment

No agents enrolled on the k3s Wazuh cluster. This workstation would be the first. Refer to section 8 of the 2026-06-24 vulnerability assessment partial for full install steps.

Key blocker: Arch is not officially supported by Wazuh — AUR or source build required.

yay -Ss wazuh-agent
Age Identity Compartmentalization

Single personal.key decrypts everything. Split into purpose-scoped identities. Refer to section 7 of the 2026-06-24 vulnerability assessment partial.

This requires re-encrypting all files with new keys — larger task, may warrant its own session.

arch-audit Daily Timer Integration

Wire arch-audit into the existing daily-audit.service systemd timer.

systemctl --user cat daily-audit.service

Review what the service runs, then add arch-audit to the script.

Work Machine Deployment

Pull dots-quantum changes (GPG agent hardening) and replicate system-level configs. Full runbook in security-hardening-deploy-work-machine.adoc (2026-06-24).

Priority Order
Priority Action Status

P2

AppArmor complain → enforce (gopass, curl, wget, python3, git)

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

P2

nftables egress — review logs, switch to policy drop

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

P2

Thunderbolt policy (bolt) — configure authorization

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

P2

Wazuh agent enrollment

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

P2

Age identity compartmentalization

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

P3

arch-audit daily timer integration

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

P3

Work machine deployment

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

PowerShell Sysadmin — Mouseless Windows Management

PowerShell Sysadmin — Mouseless Windows Management

The work laptop runs Windows with WSL (Arch). The goal is to manage Windows without reaching for the mouse — PowerShell and netsh handle what the GUI does, with the added benefit of repeatability and scripting.

Today’s session: morning boot sequence — kill bloatware processes that auto-launch, reconnect Wi-Fi after overnight disconnect.

Bulk Process Termination — Pipeline Pattern

Kill processes by name pattern without opening Task Manager. The pipeline is Get-ProcessWhere-ObjectStop-Process.

Discovery first — find the real process name

The process name is not always what you expect. Teams 2.0 registers as ms-teams, not teams. Always discover before killing:

Find processes matching a name
Get-Process | Where-Object { $_.Name -match 'teams' }
Real output
 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     67    92.12     136.34       6.27   35184   1 ms-teams
     64    96.76      87.17      16.42   35516   1 ms-teams

The ProcessName column is the truth. Use that value in your kill command — not a guess.

Alias: ? for Where-Object

Where-Object has a built-in alias — ?. Same pipeline, fewer keystrokes:

Get-Process | ? Name -match 'teams'

When using ? with the simplified syntax, the property name (Name) and operator (-match) go directly after ? without {$_.} wrapping. This only works for simple single-property filters.

Get the binary path
Get-Process | Where-Object { $_.Name -match 'teams' } | Select-Object Name, Id, Path
Real output
Name        Id Path
----        -- ----
ms-teams 35184 C:\Program Files\WindowsApps\MSTeams_26149.1205.4798.6437_x64__8wekyb3d8bbwe\ms-teams.exe
ms-teams 35516 C:\Program Files\WindowsApps\MSTeams_26149.1205.4798.6437_x64__8wekyb3d8bbwe\ms-teams.exe

Select-Object extracts specific properties from the pipeline objects — equivalent to awk '{print $1, $2, $11}' but by name instead of position. That WindowsApps path confirms Teams 2.0 is a packaged app — use Start-Process "ms-teams:" (URI handler) to launch it.

Refining the regex — exact match vs broad match
Broad match — catches anything containing "teams"
Get-Process | ? Name -match 'teams'
Anchored match — only teams or ms-teams
Get-Process | Where-Object { $_.Name -match '^(ms-)?teams$' }

^ anchors to start, $ to end. (ms-)? makes the ms- prefix optional. Same regex logic as grep -P on Linux — PowerShell’s -match is regex by default.

Kill a single application
Get-Process | Where-Object { $_.Name -match '^(ms-)?teams$' } | Stop-Process -Force
Table 1. Pipeline breakdown
Stage What it does

Get-Process

Lists all running processes — equivalent to ps aux on Linux.

Where-Object { $_.Name -match '…​' }

Filters by Name property. -match uses regex. $_ is the current pipeline object — PowerShell’s equivalent of awk’s `$0.

Stop-Process -Force

Sends a kill signal. -Force bypasses confirmation and kills child processes — equivalent to kill -9 on Linux.

Kill multiple applications — single pipeline

Repeating the pipeline per application is tedious. Match multiple names in one pass with regex alternation:

Get-Process | Where-Object { $_.Name -match 'teams|zoom' } | Stop-Process -Force
Real output before killing — verify what you’re about to stop
Get-Process | Where-Object { $_.Name -match 'teams|skype|zoom' }
 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     62    85.36      30.65       7.48   35184   1 ms-teams
     62    96.58      38.05      24.52   35516   1 ms-teams
     66   130.20      31.47   1,694.38    6724   1 Zoom
    126   267.64      30.02   1,820.80   31180   1 Zoom

Zoom at 1,694 CPU seconds — that process has been running for a while. The verify-before pattern applies here the same as on Linux: see what you’re killing before you kill it.

Linux equivalent — the contrast
pkill -f 'teams|zoom'

One command. pkill -f matches the full command line, regex by default. PowerShell’s verbosity is the price of its object pipeline — you get structured data ($.Name, $.Path, $_.Id) instead of text parsing, but you pay in keystrokes.

Wi-Fi Control — netsh wlan Disconnect and Reconnect

Manage wireless connections from the terminal. netsh wlan is a CMD utility that works in PowerShell — not a cmdlet, but it does the job without touching the mouse.

Disconnect Wi-Fi
netsh wlan disconnect interface="Wi-Fi"
Reconnect to a saved profile
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"
The radio-state gotcha

netsh wlan connect fails with "the network specified by profile is not available to connect" when the Wi-Fi radio has been soft-disabled or the adapter hasn’t finished scanning after resume from sleep/lid-close.

The fix is not a command — it’s a timing issue. The Wi-Fi adapter needs a few seconds after wake to populate its scan results. Opening the network flyout (clicking the Wi-Fi icon in the system tray) triggers a scan. Once the SSID appears in the list, netsh wlan connect succeeds without clicking anything in the flyout.

Sequence that works after lid-open
# 1. Attempt connect — fails if radio hasn't scanned yet
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"
# "the network specified by profile is not available to connect"

# 2. Left-click Wi-Fi icon in system tray — triggers adapter scan
#    Wait for SSID to appear in the list (2-5 seconds)

# 3. Re-run — succeeds
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"
# "Connection request was completed successfully."
Automating the scan trigger

A pure-CLI approach to force a scan without the mouse:

# Force Wi-Fi adapter reset (triggers fresh scan)
netsh interface set interface "Wi-Fi" disabled
Start-Sleep -Seconds 2
netsh interface set interface "Wi-Fi" enabled
Start-Sleep -Seconds 5
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"

netsh interface set interface toggles the adapter at the OS level — equivalent to ip link set wlan0 down/up on Linux. The 5-second sleep gives the radio time to scan and associate. Requires an elevated (admin) PowerShell session.

Show saved profiles and current state
List all saved Wi-Fi profiles
netsh wlan show profiles
Show current connection details
netsh wlan show interfaces
Show signal strength, channel, BSSID
netsh wlan show interfaces | Select-String "Signal|Channel|BSSID|SSID|State"

Select-String is PowerShell’s grep. Accepts regex by default.

Linux equivalent
# Disconnect
nmcli device disconnect wlan0

# Connect
nmcli device wifi connect "SSID" ifname wlan0

# Scan
nmcli device wifi rescan ifname wlan0

# Status
nmcli device wifi list

nmcli is declarative — wifi connect handles scanning, association, and authentication in one step. netsh requires you to manage the scan/connect sequence yourself.

The broader pattern

Every mouse action in Windows has a PowerShell or netsh equivalent. The friction is discovery — unlike Linux where man/--help/apropos are consistent, PowerShell’s discoverability depends on knowing the verb-noun convention:

# Find commands related to a concept
Get-Command *process*
Get-Command *network*
Get-Command *wifi*    # returns nothing — wifi is netsh territory, not PowerShell cmdlets

# Get help for a cmdlet
Get-Help Get-Process -Examples

Get-Command is PowerShell’s apropos. The verb-noun pattern (Get-Process, Stop-Service, Set-NetAdapter) is predictable once you internalize it, but netsh sits outside this system entirely — it’s a legacy CMD utility that PowerShell inherited.

The session builds toward a single startup script that runs the full morning sequence: kill bloatware, reconnect Wi-Fi, launch WSL — zero mouse clicks from power button to terminal prompt.

zfold7 Refresh — USBGuard + BYOD Cert + Repo Sync

zfold7 Refresh — Repo Sync + SSH config.d Migration

Execution order: USBGuard → ADB → BYOD cert → network access → SSH → repo sync.

Blocker: USBGuard Blocking Phone

USBGuard blocks unknown USB devices by default. The Samsung Android ADB interface (04e8:6860) must be allowed before ADB can see the phone.

Paste 1 — verify blocked devices
sudo usbguard list-devices | grep block
Paste 2 — open live monitor (Terminal 1 — leave running)
sudo journalctl -u usbguard -f | grep -P --color 'target\.new=.*(allow|block)|Device\.(Insert|Remove)' | tee /tmp/usbguard-monitor-"$(date +%F)".log
Paste 3 — allow all blocked devices (Terminal 2)
sudo usbguard list-devices | grep block | awk -F: '{print $1}' | xargs -n1 --no-run-if-empty sudo usbguard allow-device

Terminal 1 should show target.new='allow'.

Paste 4 — persist the ADB rule
sudo usbguard append-rule 'allow id 04e8:6860'
Paste 5 — verify rule persisted
sudo grep -P '04e8:(6860|6300)' /etc/usbguard/rules.conf
Paste 6 — replug test

Unplug and replug the phone. Terminal 1 should show:

Device.Remove  ...  04e8:6860
Device.Insert  ...  04e8:6860
target.new='allow'

If it shows target.new='block', the append-rule failed — check rules.conf.

Paste 7 — confirm ADB
adb devices -l
Expected
R3GYB0J7YHY  device usb:6-2 product:q7quew model:SM_F966U1 device:q7q transport_id:10
Observed (2026-06-25) — replug before rule persisted
Device.Remove  09:50:28  block id 04e8:6860 serial "R3GYB0J7YHY" name "SAMSUNG_Android" via-port "6-2"
Device.Insert  09:50:34  block id 04e8:6860 serial "R3GYB0J7YHY" name "SAMSUNG_Android" via-port "6-2"
Policy.Device.Update  target.new='block'

Same physical device, same serial, same hash — re-blocked on insert because no persistent rule existed.

Root cause — ADB rule was never persisted
sudo grep '04e8' /etc/usbguard/rules.conf
Found (2026-06-25)
allow id 04e8:6300 serial "0376025090001626" name "Type-C" ...

The storage interface (04e8:6300) was persisted from a previous session. The ADB interface (04e8:6860) was only allowed temporarily — never `append-rule’d.

Samsung USB device IDs
Vendor:Product Name Interface

04e8:6860

SAMSUNG_Android (ADB)

06:01:01 02:02:01 0a:00:00

04e8:6300

Type-C (USB storage)

08:06:50

The phone exposes two USB interfaces from one physical device. Which ones enumerate depends on the phone’s USB mode (File Transfer vs Charging Only).
Reference

See partials/worklog/daily-notes/2026-06/2026-06-24/arch-vulnerability-assessment/deep-audit-findings.adoc Paste 4 for the full USBGuard allow/block/backout workflow.

ADB Troubleshooting — USBGuard Allows but ADB Empty

If sudo usbguard list-devices | grep -i samsung shows allow but adb devices -l is empty:

Paste 1 — check udev rules exist
find /etc/udev/rules.d /usr/lib/udev/rules.d -regex '.*\(android\|adb\|samsung\).*' 2>/dev/null

If nothing returns — no udev rules for Android. ADB can’t claim the USB interface without permission.

Paste 2 — check android-udev package
pacman -Qs android-udev

If not installed:

pacman -Ss android-udev
Paste 4 — reload udev and restart ADB
sudo udevadm control --reload-rules
sudo udevadm trigger
adb kill-server
adb devices -l
Paste 5 — add user to adbusers group (if ADB still empty after install)
groups | grep adbusers
sudo usermod -aG adbusers evanusmodestus

Log out and back in (or newgrp adbusers) for the group to take effect. Then:

adb kill-server
adb devices -l
Paste 6 — check USB mode on phone
On phone: pull down notification shade → tap USB notification → select "File Transfer"
USB controlado por: Este dispositivo
Usar USB para: Transferir archivos / Android Auto
Paste 7 — check phone prompted for authorization
On phone: look for "Allow USB debugging?" popup → tap Allow
Check "Always allow from this computer"
Paste 8 — check kernel sees the device
lsusb | grep -i samsung
sudo dmesg | grep -i 'samsung\|04e8' | tail -10
Paste 9 — full USB re-enumeration
adb kill-server
sudo usbguard list-devices | grep -i samsung

Unplug, wait 5 seconds, replug. Then:

adb devices -l
Root cause (2026-06-25)

android-udev package was not installed. The kernel saw the Samsung device (04e8:6860 on usb 6-2), USBGuard allowed it, but ADB had no udev rule granting permission to claim the interface. Package creates group adbusers (GID 959) and installs rules to /usr/lib/udev/rules.d/.

Worked on Razer (also Arch) because android-udev was installed there. Never installed on P16g.

User was also not in adbusers group — added with sudo usermod -aG adbusers evanusmodestus. Confirmed with groups | grep adbusers. ADB still empty after both fixes — investigation continues.

Blocker: BYOD EAP-TLS Certificate

Cert was issued 2026-06-07 (90-day TTL, valid until ~September) but never installed on device. P12 was in /dev/shm/ (tmpfs — gone after reboot).

See Phase 8: BYOD Certs for the canonical workflow.

Locate existing P12 — three tools, same question
Tool How it searches Regex flag Speed

find

Walks filesystem live

-regex (matches full path)

Slow — reads every directory

locate

Queries pre-built database (updatedb)

-r or --regex

Fast — database lookup

grep

Searches file contents, not filenames

Pattern is first argument

Fast — but wrong tool for finding files by name

find — live filesystem walk
find /dev/shm /tmp ~ -regex '.*zfold7.*\.p12' 2>/dev/null
find ~ -regex '.*\.p12$' 2>/dev/null
locate — database lookup (faster, but stale)
sudo updatedb
locate '*.p12'
locate -r '.*zfold7.*\.p12'
locate -r '.*\.p12$'

locate does NOT take directory arguments — it searches its entire database. /dev/shm files are never indexed (tmpfs cleared before updatedb runs).

grep — search file contents for references to the P12
grep -rn 'zfold7.*\.p12\|p12.*zfold7' docs/ --include='*.adoc'
grep -rn 'serial\|zfold7-cert\.json' docs/modules/ROOT/partials/worklog/daily-notes/2026-06-0[7-9]/ --include='*.adoc'
Check if phone already has it
ssh fold7 'find /sdcard/Download -regex ".*\.p12" 2>/dev/null'
Result (2026-06-24)
find /dev/shm /tmp ~ -regex '.*zfold7.*\.p12' 2>/dev/null
(no output — P12 gone from tmpfs)

locate -r '.*\.p12$'
/home/evanusmodestus/go/pkg/mod/github.com/.../testdata/certificate.p12
(unrelated Go module test cert — not ours)

P12 confirmed gone. Re-issue from Vault required.

Re-issue from Vault
dsource d000 dev/vault
vault write pki_int/issue/domus-byod \
  common_name="zfold7-evanusmodestus.byod.inside.domusdigitalis.dev" \
  alt_names="zfold7.byod.inside.domusdigitalis.dev" \
  uri_sans="spiffe://domusdigitalis.dev/byod/zfold7-evanusmodestus" \
  ttl="2160h" \
  -format=json > /dev/shm/zfold7-cert.json
jq -r '.data.certificate' /dev/shm/zfold7-cert.json > /dev/shm/zfold7.crt
jq -r '.data.private_key' /dev/shm/zfold7-cert.json > /dev/shm/zfold7.key
jq -r '.data.ca_chain[]' /dev/shm/zfold7-cert.json > /dev/shm/zfold7-chain.pem
openssl x509 -in /dev/shm/zfold7.crt -noout -subject -dates -ext subjectAltName
Bundle P12 and push to phone
openssl pkcs12 -export \
  -out /dev/shm/zfold7-evanusmodestus.p12 \
  -inkey /dev/shm/zfold7.key \
  -in /dev/shm/zfold7.crt \
  -certfile /dev/shm/zfold7-chain.pem \
  -name "Domus BYOD EAP-TLS"
adb push /dev/shm/zfold7-evanusmodestus.p12 /sdcard/Download/
Install on Android
  1. Settings → Security → More security settings → Install from device storage

  2. Select zfold7-evanusmodestus.p12

  3. Enter the export password

  4. Name: Domus BYOD EAP-TLS

  5. Usage: WiFi

Configure WiFi profile
  1. Settings → Wi-Fi → Domus-Secure

  2. EAP method: TLS

  3. CA certificate: DOMUS-ROOT-CA

  4. User certificate: Domus BYOD EAP-TLS

  5. Identity: zfold7-evanusmodestus.byod.inside.domusdigitalis.dev

Verify in ISE — use proven scripts
dsource d000 dev/network/ise
bash $(find ~/atelier -name 'auth-history.sh' -path '*/mnt/*') 9C:83:06:CE:89:46 full 168 \
  | tee /tmp/ise-auth-zfold7-"$(date +%F)".json \
  | jq -r '.[] | "\(.TIMESTAMP_TIMEZONE)  \(.PASSED)  \(.FAILURE_REASON)"'
bash $(find ~/atelier -name 'active-sessions.sh' -path '*/ise*') \
  | tee /tmp/ise-sessions-"$(date +%F)".json \
  | jq '.activeList.activeSession[]? | select(.calling_station_id | ascii_upcase | contains("9C:83:06"))'
Cleanup
shred -vfz -n 3 /dev/shm/zfold7*
rm -f /dev/shm/zfold7*
Pre-flight — connect and verify
ssh fold7
whoami
uname -m
pwd
Step 1: Pull stale repos
git -C ~/atelier/_bibliotheca/domus-captures pull
git -C ~/.config/nvim pull
git -C ~/atelier/_projects/personal/dotfiles-optimus pull
Step 2: Clone missing repos
git clone git@github.com:EvanusModestus/dots-quantum.git ~/atelier/_projects/personal/dots-quantum
Step 3: SSH config.d migration

The phone has a flat ~/.ssh/config from Phase 1. Workstation now uses modular ~/.ssh/config.d/ with 7 files.

From workstation — push config.d to phone
scp -r ~/.ssh/config.d fold7:~/.ssh/
On phone — replace flat config with Include directive
cp ~/.ssh/config ~/.ssh/config.bak
cat > ~/.ssh/config << 'EOF'
Include config.d/*.conf
EOF
chmod 600 ~/.ssh/config
Verify — test key hosts
ssh -T git@github.com
ssh -T git@gitlab.com
ssh -T git@gitea-01
Step 4: ~/.secrets repo
mkdir -p ~/.secrets
git clone git@github.com:EvanusModestus/secrets-metadata.git ~/.secrets
ls ~/.secrets/.metadata/keys/
Step 5: Re-stow dotfiles
cd ~/atelier/_projects/personal/dotfiles-optimus/base
stow -t ~ zsh
stow -t ~ git
stow -t ~ fzf
stow -t ~ fd
stow -t ~ ripgrep
stow -t ~ vim
Step 6: gopass — verify v3 store has new schema
gopass sync
gopass show v3/domains/d000/identity/ise/ise-01 | tail -n +2 | yq 'keys'
Step 7: Verify yq variant
yq --version
gopass show v3/domains/d000/identity/ssh/github | tail -n +2 | yq 'keys'
Regex Practice — Finding Project Partials
Funnel pattern: broad → narrow with find -regex
# All zfold7 .adoc files
find docs/modules/ROOT/partials/projects -regex '.*zfold.*\.adoc$'

# Only phase files (phase + any digit)
find docs/modules/ROOT/partials/projects -regex '.*zfold.*/phase-[0-9].*\.adoc$'

# Specific phase range — phases 0 through 4
find docs/modules/ROOT/partials/projects -regex '.*zfold.*/phase-0[0-4].*\.adoc$'

# Phase 8 across two projects — alternation with \( \| \)
find docs/modules/ROOT/partials/projects -regex '.*\(zfold\|ise-home\).*/phase-08.*\.adoc$'

# Jump into the result with nvim
nvim $(find docs/modules/ROOT/partials/projects -regex '.*ise-home.*/phase-08.*\.adoc$')
Three tools for finding files — when to use each
# find — live filesystem, takes directory arguments, slow but always current
find /dev/shm /tmp ~ -regex '.*zfold7.*\.p12' 2>/dev/null

# locate — database lookup, fast but stale, NO directory arguments
sudo updatedb
locate -r '.*zfold7.*\.p12'

# grep — searches file CONTENTS not filenames, use for references
grep -rn 'zfold7.*\.p12\|p12.*zfold7' docs/ --include='*.adoc'
Checklist
Check Status

USBGuard — Samsung ADB allowed

[ ]

USBGuard — rule persisted (04e8:6860)

[ ]

ADB sees device (adb devices -l)

[ ]

P12 located or re-issued from Vault

[ ]

P12 pushed to phone via ADB

[ ]

BYOD cert installed on Android

[ ]

WiFi profile configured (EAP-TLS)

[ ]

ISE auth verified (PASSED, no failure)

[ ]

SSH to fold7 works

[ ]

domus-captures pulled

[ ]

domus-nvim pulled

[ ]

dotfiles-optimus pulled

[ ]

dots-quantum cloned

[ ]

SSH config.d/ deployed

[ ]

Include directive in ~/.ssh/config

[ ]

GitHub/GitLab/Gitea SSH verified

[ ]

~/.secrets cloned

[ ]

Dotfiles re-stowed

[ ]

gopass v3 synced + new schema visible

[ ]

yq variant confirmed

[ ]

Systemd Timer Audit — 11 User Timers

Systemd Timer Audit — 11 User Timers

Full audit of all user systemd timers (~/.config/systemd/user/*.timer). Read every timer, service unit, and backing script. Checked journals for each unit.

Scorecard
Timer Schedule Status Notes

cert-expiry-monitor

Daily 08:03

⚠️ Alerting

2 expired certs (ISE EAP-TLS 9d, Vault SSH 10d)

chla-code-alert

M-F 08:07 + 14:17

⚠️ Alerting

32 projects without PeopleSoft codes

cissp-countdown

Daily 05:30 + 20:00

✅ Healthy

51 days to exam, tier 2 messaging

daily-audit

Daily 21:07

⚠️ Alerting

17 stale .age files in mschapv2-migration

domus-health

Daily 09:00

🔧 Bug

False "missed" on 3 weekly timers

dotfiles-sync

Daily 20:30

✅ Healthy

education-pulse

Daily 06:00

✅ Healthy

Thu = Español — Inmersión Total

pacman-news

Daily 07:15

✅ Recovered

Failed 06-24 (transient), ok 06-25

project-stale

Mon 07:30

✅ Healthy

Ran Mon 06-22, next Mon 06-29

security-remediation

Mon 07:00

✅ Healthy

1/10 phases complete (10%)

weekly-review

Sun 19:00

✅ Healthy

221 commits last week

Bug — domus-health false positives

The morning health notification reported "3 missed" and project-stale: never ran. This is incorrect — all three weekly timers ran on schedule:

  • project-stale — ran Mon 06-22 07:41 (72h ago, next Mon 06-29)

  • security-remediation — ran Mon 06-22 07:41 (72h ago, next Mon 06-29)

  • weekly-review — ran Sun 06-21 19:00 (96h ago, next Sun 06-28)

Root cause: domus-health (~/.local/bin/domus-health) uses a flat 192-hour window for weekly timers but reports "never ran" for timers that last executed 72-96 hours ago. The staleness heuristic does not account for day-of-week scheduling — a Monday timer checked on Thursday is mid-cycle, not missed.

Fix needed: Compare against the timer’s OnCalendar day-of-week, or only flag weekly timers when they exceed their actual 168-hour cycle plus a 24-hour grace period.

Cert Expiry — Action Required
Certificate Expired/Expires Urgency

ISE EAP-TLS Server Cert

Expired 2026-06-16

CRITICAL — 9 days overdue

Vault SSH Signing Cert

Expired 2026-06-15

CRITICAL — 10 days overdue

ASA VPN SSL Cert

Expires 2026-07-28

WARNING — 33 days remaining

Renewal path: vault write pki_int/issue/domus-server common_name=…​

Architecture Notes

All 11 scripts source ~/.local/bin/domus-notify which provides:

  • domus_notify_tier1/2/3() — notification dispatch (maps to notify-send urgency levels)

  • domus_lock() — flock-based execution guard (prevents overlapping runs)

  • days_until() / dow_num() — date helpers

Scripts live in ~/.local/bin/ (10 scripts) and scripts/chla-code-alert.sh (1 in domus-captures). All executable, all exiting clean. No missing dependencies — w3m (used by pacman-news) is installed.

Optimization Opportunities
  1. domus-health weekly logic — fix false positives (described above)

  2. pacman-news resilience — add retry or timeout for transient network failures (failed 06-24, self-recovered)

  3. security-remediation progress — 1/10 at 10% since creation; consider whether the phase list reflects current priorities or needs pruning

Infrastructure Enumeration — k3s, Wazuh, VM Inventory

Infrastructure Enumeration — Pending Items

Resuming infrastructure work stalled since March. Priorities: k3s NAT verification, Wazuh agent enrollment, VM inventory cleanup.

Current State — Verified 2026-06-25 (End of Session)
Component Status Finding

k3s cluster (k3s-master-01)

✅ HEALTHY

Single node, Rocky 9.7, v1.34.4+k3s1, 123 days uptime

k3s NAT rule 170 (10.42.0.0/16)

⚠️ WRONG SUBNET

Pod CIDR is 10.0.0.0/x (Cilium), not 10.42.0.0/16 — rule 170 targets wrong network

Wazuh pods (all 4)

✅ RUNNING

dashboard, indexer, manager-master, manager-worker — all 1/1

Wazuh indexer

✅ HEALTHY

Cluster green, 424 shards, 10,000+ alerts from modestus-p16g indexed

Wazuh agent (modestus-p16g)

✅ ACTIVE

Agent 003, connected to wazuh-workers:1514, journald + auditd + pacman collecting

Wazuh DNS

✅ FIXED

3 A records corrected on bind-01: wazuh→.133, dashboard→.137, api→.133

Wazuh dashboard

⚠️ CERT MISMATCH

Serves cert with CN=wazuh, not CN=wazuh-dashboard — Firefox blocks, IP access works

3 svclb pods

⚠️ PENDING

svclb-dashboard, svclb-prometheus-grafana, svclb-prometheus-alertmanager — MetalLB pool likely exhausted

ise-02 (kvm-02)

SHUT OFF

Replaced by ise-01; pending virsh undefine

Wazuh DNS Service Map

Verified via reverse DNS — use hostnames, not IPs.

Service Hostname Port(s)

Wazuh Manager (API, registration, syslog)

wazuh.inside.domusdigitalis.dev

55000, 1515, 514/UDP

Wazuh Indexer (OpenSearch)

wazuh-indexer.inside.domusdigitalis.dev

9200

Wazuh Dashboard

wazuh-dashboard.inside.domusdigitalis.dev

443

Wazuh Workers (agent events)

wazuh-workers.inside.domusdigitalis.dev

1514

Traefik Ingress

traefik.inside.domusdigitalis.dev

80, 443

Step 1 — k3s Cluster Health ✅ DONE

Single control-plane node healthy. sudo /usr/local/bin/k3s kubectl required (not in sudo PATH).

ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl get nodes -o wide'
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl get pods -A -o wide'
Step 2 — Wazuh Stack Health ✅ DONE

All 4 pods running. Dashboard serves (302). Indexer authenticated via gopass.

DOMAIN=inside.domusdigitalis.dev

# Indexer query (gopass-backed, no credential exposure)
curl -sk "https://wazuh-indexer.$DOMAIN:9200/_cat/indices?v" \
  -u "admin:$(gopass show -o v3/domains/d000/k3s/wazuh/indexer)"

# Dashboard reachability
curl -sk "https://wazuh-dashboard.$DOMAIN" -o /dev/null -w "%{http_code}\n"
Wazuh Connectivity Test

Test whether this workstation can reach the manager registration and event ports.

DOMAIN=inside.domusdigitalis.dev

# Event port (1514) — agent data path
nc -zv "wazuh-workers.$DOMAIN" 1514

# Registration port (1515) — agent enrollment
nc -zv "wazuh.$DOMAIN" 1515

# API authentication (55000)
curl -sk "https://wazuh.$DOMAIN:55000/security/user/authenticate" \
  -u "wazuh-wui:$(gopass show -o v3/domains/d000/k3s/wazuh/api)"

# Indexer health (9200)
curl -sk "https://wazuh-indexer.$DOMAIN:9200/_cluster/health?pretty" \
  -u "admin:$(gopass show -o v3/domains/d000/k3s/wazuh/indexer)"

# Dashboard reachability (443)
curl -sk "https://wazuh-dashboard.$DOMAIN" -o /dev/null -w "%{http_code}\n"
Step 4 — VM Inventory Verification
for host in kvm-01 kvm-02; do
  printf "\n=== %s ===\n" "$host"
  ssh "$host" 'sudo virsh list --all'
done
Step 5 — Stale VM Cleanup
Delete ise-02 (replaced by ise-01 on kvm-01)
ssh kvm-02 'sudo virsh undefine ise-02 --remove-all-storage'
Priority Order (Updated — End of Session)
Priority Action Status

P0

k3s cluster health check

✅ Done

P0

Wazuh stack health — pods, dashboard, indexer

✅ Done

P0

Agent connectivity — test 1515/1514 from workstation

✅ Done

P0

Wazuh DNS fix — 3 A records corrected on bind-01

✅ Done

P0

Wazuh agent enrollment — modestus-p16g (CR-2026-06-25)

✅ Done — agent 003 active, 10,000+ events indexed

P0

Wazuh API scripts — 8 gopass-backed verification tools

✅ Done — data/d000/infra/wazuh-api/

P0

k3s NAT — pod CIDR is 10.0.0.0/x not 10.42.0.0/16, rule 170 is wrong

⚠️ Needs review

P1

VM inventory verification — kvm-01 + kvm-02

[ ]

P1

ise-02 stale VM deletion

[ ]

P1

Rotate Wazuh registration password from default

[ ]

P1

Wazuh dashboard TLS cert — CN mismatch (wazuh vs wazuh-dashboard)

[ ]

P1

Wazuh alert tuning — reduce AppArmor/LSM noise (rule 80730)

[ ]

P2

k3s-master-02/03 status — confirm running or decommissioned

[ ]

P2

3 pending svclb pods — MetalLB IP pool audit

[ ]

P2

AppArmor wazuh-agentd — complain → enforce (48h observation)

[ ]

P2

Rootcheck definition files — download from Wazuh GitHub

[ ]

P2

Enroll remaining agents — kvm-01, kvm-02, bind-01, bind-02, ipa-01, vault-01

[ ]

Wazuh DNS A Record Fix

DNS records for Wazuh services pointed to wrong MetalLB external IPs. Discovered by comparing dig results against kubectl get svc -n wazuh -o wide output.

Table 2. DNS records — before fix
Record Had Should Be Reason

wazuh

10.50.1.132

10.50.1.133

Manager service (55000, 1515, 514/UDP)

wazuh-dashboard

10.50.1.133

10.50.1.137

Dashboard service (443)

wazuh-api

10.50.1.134

10.50.1.133

API lives on manager, not workers

Fix Applied

Zone file on bind-01: /var/named/inside.domusdigitalis.dev.zone

# Before
sudo awk 'NR>=24 && NR<=28' /var/named/inside.domusdigitalis.dev.zone

# Fix serial + 3 A records
sudo sed -i 's/2026061501/2026062501/' /var/named/inside.domusdigitalis.dev.zone
sudo sed -i '25s/10.50.1.132/10.50.1.133/' /var/named/inside.domusdigitalis.dev.zone
sudo sed -i '26s/10.50.1.133/10.50.1.137/' /var/named/inside.domusdigitalis.dev.zone
sudo sed -i '27s/10.50.1.134/10.50.1.133/' /var/named/inside.domusdigitalis.dev.zone

# After
sudo awk 'NR==3 || (NR>=24 && NR<=28)' /var/named/inside.domusdigitalis.dev.zone

# Reload
sudo rndc reload
Verification

From workstation — all five Wazuh DNS records resolving correctly:

for svc in wazuh wazuh-workers wazuh-dashboard wazuh-indexer wazuh-api; do
  printf "%-20s %s\n" "$svc" "$(dig +short $svc.inside.domusdigitalis.dev)"
done
Table 3. DNS resolution — after fix
Record IP Status

wazuh

10.50.1.133

Pass

wazuh-workers

10.50.1.134

Pass

wazuh-dashboard

10.50.1.137

Pass

wazuh-indexer

10.50.1.130

Pass

wazuh-api

10.50.1.133

Pass

Post-Fix Connectivity

All four tests that previously failed now succeed:

DOMAIN=inside.domusdigitalis.dev
nc -zv -w3 "wazuh.$DOMAIN" 1515
nc -zv -w3 "wazuh.$DOMAIN" 55000
curl -sk "https://wazuh.$DOMAIN:55000/security/user/authenticate" \
  -u "wazuh-wui:$(gopass show -o v3/domains/d000/k3s/wazuh/api)"
curl -sk "https://wazuh-dashboard.$DOMAIN" -o /dev/null -w "Dashboard HTTP: %{http_code}\n"
Table 4. Results
Test Result

nc wazuh:1515

Connection succeeded

nc wazuh:55000

Connection succeeded

API authenticate

JWT token returned (error: 0)

Dashboard HTTPS

HTTP 302

Wazuh Agent Install — Arch Linux (RPM Extraction)

Official install script does not support Arch (RPM/DEB only). Agent installed by extracting the 4.14.3 RPM with bsdtar. Full CR: CR-2026-06-25

Pre-flight — connectivity + backup
DOMAIN=inside.domusdigitalis.dev

# Verify ports
nc -zv "wazuh.$DOMAIN" 1515
nc -zv "wazuh-workers.$DOMAIN" 1514
curl -sk -o /dev/null -w '%{http_code}' "https://wazuh.$DOMAIN:55000/"

# Backup security configs
BACKUP_DIR="/tmp/pre-wazuh-backup-$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
sudo cp -a /etc/apparmor.d/ "$BACKUP_DIR/apparmor.d/"
sudo cp -a /etc/nftables.conf "$BACKUP_DIR/nftables.conf"
sudo aa-status > "$BACKUP_DIR/apparmor-status.txt" 2>&1
sudo nft list ruleset > "$BACKUP_DIR/nftables-ruleset.txt" 2>&1
GPG key import
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH -o /tmp/wazuh-gpg-key.asc
gpg --import /tmp/wazuh-gpg-key.asc
gpg --list-keys | grep -i wazuh
Version match — agent must match manager
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl exec -n wazuh wazuh-manager-master-0 -- /var/ossec/bin/wazuh-control info 2>/dev/null | head -5'

Result: WAZUH_VERSION="v4.14.3" — agent must be 4.14.3.

Install script investigation (failed)

CDN 4.x path returns AccessDenied. 4.14 path serves the script but only supports apt/yum/dnf/zypper — no pacman.

curl -sI https://packages.wazuh.com/4.14/wazuh-install.sh | head -3
curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh
grep -i 'arch\|pacman\|apt\|yum\|dnf\|zypper' wazuh-install.sh | head -20
RPM extraction — the actual install method
cd /tmp
curl -sO "https://packages.wazuh.com/4.x/yum/wazuh-agent-4.14.3-1.x86_64.rpm"
file wazuh-agent-4.14.3-1.x86_64.rpm

mkdir -p /tmp/wazuh-staging
cd /tmp/wazuh-staging
bsdtar -xf /tmp/wazuh-agent-4.14.3-1.x86_64.rpm

# Install
sudo cp -a ./var/ossec /var/ossec
sudo cp ./usr/lib/systemd/system/wazuh-agent.service /usr/lib/systemd/system/
sudo groupadd -r wazuh 2>/dev/null
sudo useradd -r -g wazuh -d /var/ossec -s /sbin/nologin wazuh 2>/dev/null
sudo chown -R root:wazuh /var/ossec
sudo chown -R wazuh:wazuh /var/ossec/queue /var/ossec/var /var/ossec/logs /var/ossec/tmp
sudo chmod -R 770 /var/ossec/queue /var/ossec/var /var/ossec/logs /var/ossec/tmp
sudo chmod +x /var/ossec/bin/*
sudo chmod +x /var/ossec/active-response/bin/*
sudo systemctl daemon-reload

# Verify
sudo /var/ossec/bin/wazuh-agentd -V
systemctl status wazuh-agent 2>&1 | head -5
Wazuh 4.14.x uses group wazuh not ossec. Queue/var/logs/tmp need wazuh:wazuh ownership with 770 permissions or the agent fails with Queue not accessible: Permission denied.
Configure ossec.conf

No default ossec.conf in the RPM — write fresh. Arch uses journald exclusively (no /var/log/auth.log, /var/log/syslog, /var/log/nftables.log).

sudo tee /var/ossec/etc/ossec.conf > /dev/null << 'OSSEC_EOF'
<ossec_config>
  <client>
    <server>
      <address>wazuh-workers.inside.domusdigitalis.dev</address>
      <port>1514</port>
      <protocol>tcp</protocol>
    </server>
    <enrollment>
      <enabled>yes</enabled>
      <manager_address>wazuh.inside.domusdigitalis.dev</manager_address>
      <port>1515</port>
      <agent_name>modestus-p16g</agent_name>
    </enrollment>
    <notify_time>10</notify_time>
    <time-reconnect>60</time-reconnect>
  </client>
  <localfile>
    <log_format>journald</log_format>
  </localfile>
  <localfile>
    <log_format>audit</log_format>
    <location>/var/log/audit/audit.log</location>
  </localfile>
  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/pacman.log</location>
  </localfile>
  <syscheck>
    <disabled>no</disabled>
    <frequency>43200</frequency>
    <directories check_all="yes" realtime="yes">/etc,/usr/bin,/usr/sbin</directories>
    <directories check_all="yes">/boot</directories>
    <ignore>/etc/mtab</ignore>
    <ignore>/etc/hosts.deny</ignore>
    <ignore>/etc/mail/statistics</ignore>
    <ignore>/etc/random-seed</ignore>
    <ignore>/etc/adjtime</ignore>
    <ignore>/etc/resolv.conf</ignore>
  </syscheck>
  <rootcheck>
    <disabled>no</disabled>
    <check_files>yes</check_files>
    <check_trojans>yes</check_trojans>
    <check_dev>yes</check_dev>
    <check_sys>yes</check_sys>
    <check_pids>yes</check_pids>
    <check_ports>yes</check_ports>
    <check_if>yes</check_if>
    <frequency>43200</frequency>
  </rootcheck>
  <active-response>
    <disabled>no</disabled>
  </active-response>
  <logging>
    <log_format>plain</log_format>
  </logging>
</ossec_config>
OSSEC_EOF
<server><address> points to wazuh-workers (1514 event delivery), NOT wazuh (manager). Manager handles registration (1515) only. Getting this wrong causes Transport endpoint is not connected.
Manual registration with agent-auth

Auto-enrollment via systemctl start causes duplicate agent race condition. Use agent-auth for one-shot registration.

# Write registration password
echo -n "password" | sudo tee /var/ossec/etc/authd.pass > /dev/null
sudo chmod 640 /var/ossec/etc/authd.pass
sudo chown root:wazuh /var/ossec/etc/authd.pass

# Register (one-shot — -P takes string, not file path)
sudo /var/ossec/bin/agent-auth \
  -m wazuh.inside.domusdigitalis.dev \
  -p 1515 \
  -P password \
  -A modestus-p16g

# Verify key written
sudo awk '{print "id="$1, "name="$2, "key_fields="NF}' /var/ossec/etc/client.keys
Registration password is password (Wazuh default). Rotate immediately.
If duplicate agent error — cleanup
DOMAIN=inside.domusdigitalis.dev

sudo systemctl stop wazuh-agent

TOKEN=$(curl -sk -u "wazuh-wui:$(gopass show -o v3/domains/d000/k3s/wazuh/api)" \
  -X POST "https://wazuh.$DOMAIN:55000/security/user/authenticate" | jq -r '.data.token')

# List agents
curl -sk -H "Authorization: Bearer $TOKEN" \
  "https://wazuh.$DOMAIN:55000/agents" | jq '.data.affected_items[] | {id, name, status}'

# Delete stale (replace AGENT_ID)
curl -sk -H "Authorization: Bearer $TOKEN" \
  -X DELETE "https://wazuh.$DOMAIN:55000/agents?agents_list=AGENT_ID&status=all&older_than=0s" | jq '.message'

# Clear local keys and re-register
sudo truncate -s 0 /var/ossec/etc/client.keys
sudo /var/ossec/bin/agent-auth -m wazuh.$DOMAIN -p 1515 -P password -A modestus-p16g
Start agent and verify
sudo systemctl start wazuh-agent
sudo tail -15 /var/ossec/logs/ossec.log
sudo grep -i 'connected' /var/ossec/logs/ossec.log | tail -3

Expected: Connected to the server ([wazuh-workers.inside.domusdigitalis.dev]:1514/tcp)

AppArmor profile (complain mode)
sudo aa-status 2>&1 | grep -i 'wazuh\|ossec'

Expected: /var/ossec/bin/wazuh-agentd in complain mode. Enforce after 48h observation.

Result
bash $(find data/d000/infra/wazuh-api -name 'wazuh-agents.sh')

Agent 003 modestus-p16g — status active, version Wazuh v4.14.3, connected to workers on 1514.

Wazuh API Verification

Scripts at data/d000/infra/wazuh-api/. All output to /tmp/wazuh-*.json. All credentials via gopass.

Health dashboard
bash $(find data/d000/infra/wazuh-api -name 'wazuh-cluster-health.sh')
Agent list with OS/version/status
bash $(find data/d000/infra/wazuh-api -name 'wazuh-agents.sh')
Agent detail — specific agent
bash $(find data/d000/infra/wazuh-api -name 'wazuh-agent-detail.sh') modestus-p16g
Alerts — all agents or filtered
# Last 10 alerts, all agents
bash $(find data/d000/infra/wazuh-api -name 'wazuh-alerts.sh')

# Last 10 from modestus-p16g
bash $(find data/d000/infra/wazuh-api -name 'wazuh-alerts.sh') 10 modestus-p16g

# Last 20 from any agent
bash $(find data/d000/infra/wazuh-api -name 'wazuh-alerts.sh') 20
File Integrity Monitoring events
bash $(find data/d000/infra/wazuh-api -name 'wazuh-fim-events.sh') modestus-p16g
Delete stale agent
bash $(find data/d000/infra/wazuh-api -name 'wazuh-delete-agent.sh') AGENT_ID
Test alert — trigger and verify pipeline end-to-end
# Trigger failed SSH login (Wazuh rule 5710)
ssh -o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 fakeuser@localhost 2>/dev/null; true

# Wait for propagation
sleep 30

# Check if alert appeared from modestus-p16g
bash $(find data/d000/infra/wazuh-api -name 'wazuh-alerts.sh') 5 modestus-p16g
Wazuh Alert Tuning — Light Tune

10,000+ events/hour from two sources identified via indexer query.

Noise source 1 — gopass/gpg AppArmor ALLOWED (suppress)

Routine apparmor="ALLOWED" events from gopass calling gpgconf/gpg. Library loads, locale files, keyboxd socket access. Fires every time API scripts run or git hooks trigger gopass. Harmless — suppress on manager.

Noise source 2 — wazuh-agentd merged.mg EACCES (fix, don’t suppress)

wazuh-agentd tries to create /var/ossec/etc/shared/merged.mg (manager-pushed shared config) and fails with exit=-13 (EACCES). Directory permissions block it (root:wazuh, mode 040750). This is a real issue — fix permissions, keep the alert visible.

Step 1 — Fix agent shared directory permissions
# Before
sudo ls -la /var/ossec/etc/ | grep shared

# Fix
sudo chmod 770 /var/ossec/etc/shared
sudo chown wazuh:wazuh /var/ossec/etc/shared

# After
sudo ls -la /var/ossec/etc/ | grep shared
Step 2 — Write custom rule to suppress gopass noise
Write the complete local_rules.xml (preserves existing default rule)
tee /tmp/wazuh-local-rules.xml << 'EOF'
<!-- Local rules -->
<!-- Copyright (C) 2015, Wazuh Inc. -->

<group name="local,syslog,sshd,">
  <rule id="100001" level="5">
    <if_sid>5716</if_sid>
    <srcip>1.1.1.1</srcip>
    <description>sshd: authentication failed from IP 1.1.1.1.</description>
    <group>authentication_failed,pci_dss_10.2.4,pci_dss_10.2.5,</group>
  </rule>
</group>

<group name="local,audit,apparmor,">
  <!-- Suppress ALL AppArmor ALLOWED events — DENIED is the security signal -->
  <rule id="100010" level="0">
    <if_sid>80730</if_sid>
    <regex>apparmor=.ALLOWED.</regex>
    <description>Suppressed: routine AppArmor ALLOWED operation (all profiles)</description>
  </rule>
</group>
EOF
Step 3 — Copy rule to manager pod and restart
scp /tmp/wazuh-local-rules.xml k3s-master-01:/tmp/wazuh-local-rules.xml
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl cp /tmp/wazuh-local-rules.xml wazuh/wazuh-manager-master-0:/var/ossec/etc/rules/local_rules.xml -c wazuh-manager'
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl exec -n wazuh -c wazuh-manager wazuh-manager-master-0 -- cat /var/ossec/etc/rules/local_rules.xml'
ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl exec -n wazuh -c wazuh-manager wazuh-manager-master-0 -- /var/ossec/bin/wazuh-control restart'
Step 4 — Verify
Wait 60 seconds, then check alert volume dropped
sleep 60
bash $(find data/d000/infra/wazuh-api -name 'wazuh-alerts.sh') 10 modestus-p16g

Expected: gopass/gpg ALLOWED events no longer appear. wazuh-agentd events should stop after the permission fix. Real alerts (DENIED, level 5+) still flow.

Identify noise sources — the query that found this
jq '[.hits.hits[]._source | select(.rule.id == "80730") |
  .full_log // .data.audit.comm // .data.audit.exe] |
  group_by(.) | map({process: .[0], count: length}) |
  sort_by(-.count)' /tmp/wazuh-alerts.json
Full Environment Sweep

Covers hypervisor inventory, k3s cluster state, Wazuh service exposure, internal port check, agent list, workstation connectivity, indexer health, MetalLB pool, NAT rules, and pod CIDR verification.

Write sweep script to /tmp for editing
tee /tmp/domus-infra-sweep.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

DOMAIN=inside.domusdigitalis.dev
K3S="ssh k3s-master-01 'sudo /usr/local/bin/k3s kubectl'"

echo "=== HYPERVISOR INVENTORY ==="
for host in kvm-01 kvm-02; do
  printf "\n--- %s ---\n" "$host"
  ssh "$host" 'sudo virsh list --all'
done

echo -e "\n=== K3S CLUSTER ==="
eval $K3S get nodes -o wide
echo ""
eval $K3S get pods -A -o wide
echo ""
eval $K3S get svc -A -o wide

echo -e "\n=== WAZUH SERVICES (WHY 1515/55000 FAIL) ==="
eval $K3S get svc -n wazuh -o wide
echo ""
eval $K3S describe svc -n wazuh wazuh | grep -E 'Port|NodePort|External|Endpoint'

echo -e "\n=== WAZUH INTERNAL PORT CHECK ==="
eval $K3S exec -n wazuh wazuh-manager-master-0 -- ss -tlnp 2>/dev/null | grep -E '1515|55000|1514|514'

echo -e "\n=== WAZUH API INTERNAL ==="
eval $K3S exec -n wazuh wazuh-manager-master-0 -- curl -sk https://localhost:55000 -o /dev/null -w "API HTTP: %{http_code}\n" 2>/dev/null

echo -e "\n=== WAZUH AGENT LIST ==="
eval $K3S exec -n wazuh wazuh-manager-master-0 -- /var/ossec/bin/agent_control -l 2>/dev/null

echo -e "\n=== CONNECTIVITY FROM WORKSTATION ==="
nc -zv -w3 "wazuh-workers.$DOMAIN" 1514 2>&1
nc -zv -w3 "wazuh.$DOMAIN" 1515 2>&1
nc -zv -w3 "wazuh.$DOMAIN" 55000 2>&1
curl -sk "https://wazuh-dashboard.$DOMAIN" -o /dev/null -w "Dashboard HTTP: %{http_code}\n"
curl -sk --max-time 5 "https://wazuh.$DOMAIN:55000/security/user/authenticate" \
  -u "wazuh-wui:$(gopass show -o v3/domains/d000/k3s/wazuh/api)" 2>&1 | head -1

echo -e "\n=== WAZUH INDEXER ==="
curl -sk "https://wazuh-indexer.$DOMAIN:9200/_cluster/health?pretty" \
  -u "admin:$(gopass show -o v3/domains/d000/k3s/wazuh/indexer)"

echo -e "\n=== METALLB CONFIG ==="
eval $K3S get ipaddresspool -A -o wide 2>/dev/null || eval $K3S get configmap -n metallb-system -o yaml 2>/dev/null | grep -A5 'address-pools'
echo ""
eval $K3S get svc -A -o wide | grep LoadBalancer

echo -e "\n=== NAT RULE 170 (VYOS) ==="
ssh vyos-01 'show nat source rules' 2>/dev/null

echo -e "\n=== POD CIDR VERIFICATION ==="
eval $K3S get pods -A -o wide | awk 'NR>1 {print $7}' | sort -t. -k1,1n -k2,2n -u | head -5

echo -e "\n=== K3S SERVICE STATUS ==="
ssh k3s-master-01 'systemctl is-active k3s; uptime'

echo "=== DONE ==="
EOF
Run (or edit first: nvim /tmp/domus-infra-sweep.sh)
bash /tmp/domus-infra-sweep.sh
Domus Infrastructure Project — Build & View

Three diagrams: Graphviz topology, D2 service dependencies, D2 radial v7 (from domus-infra-ops).

Write build script to /tmp for editing
tee /tmp/build-domus-infra.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

PRJ=data/d000/projects/domus-infrastructure

# Graphviz topology
dot -Tsvg "$PRJ/diagrams/infrastructure-topology.dot" \
  -o "$PRJ/output/infrastructure-topology.svg"

# D2 service dependencies (dark theme)
d2 --theme 200 "$PRJ/diagrams/service-dependencies.d2" \
  "$PRJ/output/service-dependencies.svg"

# D2 radial v7 (dark theme)
d2 --theme 200 "$PRJ/diagrams/infrastructure-radial-v7.d2" \
  "$PRJ/output/infrastructure-radial-v7.svg"

# Build standalone HTML
build-adoc $(find "$PRJ" -maxdepth 1 -name 'domus-infrastructure.adoc') \
  html --variant light-cyan -a env-d000

# Open all in firefox
firefox $(find "$PRJ/output" -name 'domus-infrastructure.html') \
  "$PRJ/output/infrastructure-topology.svg" \
  "$PRJ/output/service-dependencies.svg" \
  "$PRJ/output/infrastructure-radial-v7.svg" &
EOF
Run (or edit first: nvim /tmp/build-domus-infra.sh)
bash /tmp/build-domus-infra.sh

P16g Monitor Disconnect Freeze (INC-2026-06-25-002)

P16g Complete Freeze on External Monitor Disconnect (INC-2026-06-25-002)

ThinkPad P16g locked up hard after disconnecting external monitor via USB-C. No tty switch, no SSH, no network — required hard power cycle. This is the third display-related incident on this machine in one month, following the i915 PSR hang (INC-2026-05-23-002) and the WAYLAND-1 phantom monitor.

Investigation Commands
Check previous boot for GPU/DRM errors
journalctl -b -1 --no-pager | grep -iE 'nvidia|i915|drm|gpu|hang|fault|error' | tail -50
All errors from crashed boot
journalctl -b -1 -p err --no-pager | tail -30
Verify i915.enable_psr=0 is active
cat /proc/cmdline
Check for WAYLAND-1 phantom monitor
hyprctl monitors -j | jq '.[].name'
Hot-unplug events in crashed boot journal
journalctl -b -1 --no-pager | grep -iE 'hotplug|unplug|disconnect|connector' | tail -20
Kernel deadlock indicators (hung task, RCU stall, lockup)
journalctl -b -1 --no-pager | grep -iE 'hung_task|rcu.*stall|soft.*lockup|hard.*lockup|watchdog' | tail -20

AppArmor curl Profile — CA Cert Read Denied (INC-2026-06-25)

AppArmor curl Profile — CA Cert Read Denied (INC-2026-06-25)

ISE ERS and MnT API calls fail with curl: (77) error adding trust anchors from file. Root cause: curl AppArmor profile (deployed 2026-06-24) has deny @{HOME}/.secrets/** r, blocking CA cert reads. DataConnect unaffected (no AppArmor profile on Python).

Prerequisites
Load ISE credentials and helpers
dsource d000 dev/network/ise
source data/shared/scripts/ise-helpers.sh
Reproduce
ERS API call — fails
ers "/endpoint?filter=mac.EQ.A8:2B:DD:8F:23:E6" | jq '.SearchResult.total'
MnT API call — fails
mnt "/Session/ActiveCount" | xq '.'
full-endpoint-profile.sh — ERS and MnT sections fail, DataConnect works
MAC="A8:2B:DD:8F:23:E6" . $(find data -name 'full-endpoint-profile.sh')
Error message (misleading — says "trust anchors" not "permission denied")
curl: (77) error adding trust anchors from file: /home/evanusmodestus/.secrets/certs/d000/ise/ROOT-CA.crt
Diagnosis
The command that finds the real cause — strace shows the syscall failure
strace -e openat curl --cacert "$ISE_CA_CERT" https://${ISE_PAN_FQDN} 2>&1 | grep -i 'secrets\|EACCES'
Result — EACCES (Permission denied) on both openat attempts
openat(AT_FDCWD, "/home/evanusmodestus/.secrets/certs/d000/ise/ROOT-CA.crt", O_RDONLY) = -1 EACCES (Permission denied)
openat(AT_FDCWD, "/home/evanusmodestus/.secrets/certs/d000/ise/ROOT-CA.crt", O_RDONLY) = -1 EACCES (Permission denied)
curl: (77) error adding trust anchors from file: /home/evanusmodestus/.secrets/certs/d000/ise/ROOT-CA.crt

EACCES on openat means the kernel refused the file read. Not a cert problem — a policy problem. strace bypasses the application’s error message and shows what actually happened at the syscall layer.

Confirm curl is in AppArmor enforce mode
sudo aa-status | grep curl
Result
   /usr/bin/curl
Find the blocking rule
sudo grep -n 'secrets' /etc/apparmor.d/usr.bin.curl
Result — line 33, blanket deny on all of ~/.secrets/
33:  deny @\{HOME}/.secrets/** r,
Elimination evidence
Table 5. Summary of tests that proved it’s not the cert, not curl, not connectivity (output — not commands)
Test Result

curl -sk (bypass cert verify)

SearchResult.total = 1 — ERS responds, connectivity + creds fine

openssl x509 -in $ISE_CA_CERT

Issuer=DOMUS-ROOT-CA, Subject=DOMUS-ISSUING-CA, CA:TRUE

file $ISE_CA_CERT

PEM certificate — valid format

stat $ISE_CA_CERT

Modify: 2026-02-09 — unchanged since February

curl --cacert /etc/ssl/certs/ca-certificates.crt

HTTP 301 — system CA bundle works with --cacert

curl --version

8.20.0 / OpenSSL 3.6.3

Why this was hard to find
Behavior Explanation

dmesg showed nothing

Explicit deny rules in AppArmor are silent — no audit log entries. Only implicit denials (missing allow rules) produce log entries.

aa-complain didn’t help

The deny keyword overrides complain mode. AppArmor design: deny means "always deny, never log." To test, you’d need to remove the deny line entirely.

Error said "trust anchors"

curl translates EACCES into its own error message. The application never sees "permission denied" — it sees "file couldn’t be loaded" and guesses it’s a cert format issue.

openssl and cat read the file fine

No AppArmor profiles on those binaries — only /usr/bin/curl is confined.

Fix
Pre-change — backup
sudo cp /etc/apparmor.d/usr.bin.curl /etc/apparmor.d/usr.bin.curl.bak.$(date +%F)
ls -la /etc/apparmor.d/usr.bin.curl*
Fix attempt 1 — FAILED (documented for learning)

Added an owner allow rule above the deny line:

# DO NOT USE — deny overrides allow in AppArmor
# sudo sed -i '/deny @\{HOME}\/.secrets\/\*\* r,/i\  owner @\{HOME}/.secrets/certs/** r,' /etc/apparmor.d/usr.bin.curl

This does NOT work. AppArmor’s deny keyword is absolute — no allow or owner rule can override it. The deny always wins regardless of rule order. This is a fundamental AppArmor design decision that differs from firewall ACLs (where order matters) and SELinux (where allow can override deny via booleans).

Fix attempt 2 — SUCCEEDED

Replace the single blanket deny with specific denies that exclude the certs path. No deny covers ~/.secrets/certs/ anymore — instead, each sensitive subdirectory is denied individually.

Restore from backup (clean slate after failed attempt 1)
sudo cp /etc/apparmor.d/usr.bin.curl.bak.$(date +%F) /etc/apparmor.d/usr.bin.curl
Verify restored to original
sudo grep -n 'secrets' /etc/apparmor.d/usr.bin.curl
Apply the correct fix — replace blanket deny with specific denies + cert allow
sudo sed -i 's|  deny @\{HOME}/\.secrets/\*\* r,|  # Deny sensitive secrets — but allow CA certs for API tools\n  deny @\{HOME}/.secrets/.metadata/** r,\n  deny @\{HOME}/.secrets/gopass/** r,\n  deny @\{HOME}/.secrets/vaults/** r,\n  owner @\{HOME}/.secrets/certs/** r,|' /etc/apparmor.d/usr.bin.curl
Verify the edit
sudo grep -n 'secrets' /etc/apparmor.d/usr.bin.curl
Expected result — specific denies + cert allow, no blanket deny
34:  # Deny sensitive secrets — but allow CA certs for API tools
35:  deny @\{HOME}/.secrets/.metadata/** r,
36:  deny @\{HOME}/.secrets/gopass/** r,
37:  deny @\{HOME}/.secrets/vaults/** r,
38:  owner @\{HOME}/.secrets/certs/** r,
Remove old profile from kernel and load the new one
sudo apparmor_parser -R /etc/apparmor.d/usr.bin.curl && sudo apparmor_parser -a /etc/apparmor.d/usr.bin.curl
Verify still in enforce mode
sudo aa-status | grep curl
Post-change verification — PASSED
MnT API — works (was failing)
mnt "/Session/ActiveCount" | xq '.'
Result — 4 active sessions returned
{
  "sessionCount": {
    "count": "4"
  }
}
Full endpoint profile — all 4 sections return data
MAC="A8:2B:DD:8F:23:E6" . $(find data -name 'full-endpoint-profile.sh')
Private keys still DENIED (security preserved)
curl file://$HOME/.secrets/.metadata/keys/master.age.key 2>&1 | head -1
Result — access denied, deny rules working
curl: (37) Could not open file /home/evanusmodestus/.secrets/.metadata/keys/master.age.key
strace confirms no more EACCES on cert path
strace -e openat curl --cacert "$ISE_CA_CERT" https://${ISE_PAN_FQDN} 2>&1 | grep -i 'EACCES'
Check AppArmor log
sudo dmesg | grep -i 'apparmor.*curl' | tail -5
Rollback
sudo cp /etc/apparmor.d/usr.bin.curl.bak.$(date +%F) /etc/apparmor.d/usr.bin.curl
sudo apparmor_parser -R /etc/apparmor.d/usr.bin.curl && sudo apparmor_parser -a /etc/apparmor.d/usr.bin.curl
sudo grep -n 'secrets' /etc/apparmor.d/usr.bin.curl
What was learned
Concept Lesson

strace -e openat

The definitive diagnostic for file access failures. Shows the syscall, the path, and the kernel’s response (EACCES, ENOENT, etc.). Bypasses application-level error messages that hide the real cause.

AppArmor deny is absolute

Cannot be overridden by allow, owner, or rule ordering. This differs from firewall ACLs (order-dependent) and iptables (-j ACCEPT before -j DROP). The only way to "allow through a deny" is to remove or narrow the deny itself.

AppArmor deny is silent

Explicit deny rules produce NO audit log entries — dmesg, journalctl, aa-logprof all show nothing. Only implicit denials (missing allow rules) are logged. This makes deny rules invisible to standard AppArmor debugging tools.

deny overrides complain mode

aa-complain has no effect on deny rules. They enforce even in complain mode. To test whether a deny rule is causing a problem, you must remove the rule entirely — complain mode is not a safe test.

Granular deny > blanket deny

deny @{HOME}/.secrets/** r, is correct intent but wrong scope. Replace with specific denies per subdirectory to preserve security while allowing legitimate reads (CA certs are public trust anchors, not secrets).

Application errors lie

curl said "error adding trust anchors" — implying a cert format problem. The real cause was a kernel-level permission denial. Always verify with strace before trusting application error messages.

Security hardening breaks things

The AppArmor profile was deployed yesterday as part of a vulnerability assessment. It worked for everything except ISE API calls because the cert path was inside the denied ~/.secrets/ tree. Security changes need testing against all operational workflows, not just the obvious ones.

AppArmor vs SELinux — quick comparison for your colleague
Feature AppArmor SELinux

Model

Path-based — rules reference file paths

Label-based — rules reference security contexts (labels on files/processes)

Default stance

Permissive (allow unless denied) — profiles opt-in

Restrictive (deny unless allowed) — system-wide mandatory

deny behavior

Absolute, silent, overrides complain mode

dontaudit for silent deny, otherwise logged. Booleans can toggle allow/deny.

Debugging

aa-logprof, dmesg (only for implicit denials), strace

audit2allow, ausearch, sealert — comprehensive audit trail

Distro default

Ubuntu, SUSE, Arch (optional)

RHEL, Fedora, CentOS

Learning curve

Lower — path-based rules are intuitive

Higher — label assignment and policy modules are complex

This incident

Silent deny produced misleading curl error. strace was required.

Would have logged the denial in audit log. audit2allow would have suggested the fix.

Both enforce Mandatory Access Control (MAC) — the kernel decides access regardless of file permissions (chmod). Traditional Unix permissions (DAC) check user/group/other. MAC checks policy on top of DAC. A file can be chmod 644 and still be denied by AppArmor/SELinux.

Appendix — study resources
Man pages (read in this order)
man apparmor.d
man apparmor_parser
man aa-status
man aa-complain
man aa-enforce
man aa-logprof
Resource Why

man apparmor.d

Profile syntax — rules, file globs, deny, owner, #include, abstractions. This is the critical page. Explains why deny is absolute.

man apparmor_parser

How profiles are loaded (-a), replaced (-r), removed (-R) from the kernel. What you used to fix this incident.

man aa-status

Shows which profiles are loaded, which are enforced vs complain, which processes are confined.

man aa-complain / man aa-enforce

Toggle modes. Now you know complain mode does NOT affect deny rules — this page confirms it.

man aa-logprof

Interactive tool that reads AppArmor logs and suggests profile changes. Only works for implicit denials (logged), not explicit deny (silent).

Arch Wiki — AppArmor

Practical setup guide, Arch-specific. Covers installation, boot params, profile management, and aa-notify.

This partial

Real incident with real syscall traces, two fix attempts (failed + succeeded), and the deny vs allow precedence discovery. Better than synthetic examples.

PCRE Lookahead — Multi-Term Same-Line Search

Finding lines where multiple terms ALL appear — regardless of order. Uses PCRE lookaheads (grep -P) to assert each term exists without consuming the line.

The core pattern — lookahead conjunction
grep -rnP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/
Table 6. Regex breakdown
Token What it does

(?=.*EAP)

Lookahead — asserts EAP exists somewhere on the line. Does NOT consume characters — the cursor stays at position 0.

(?=.*TEAP)

Second lookahead — asserts TEAP also exists. Still at position 0.

(?=.*802\.1X)

Third lookahead — asserts 802.1X also exists. \. matches literal dot.

Combined

All three must be true for the line to match. Order on the line is irrelevant — 802.1X EAP-TEAP matches the same as EAP-TEAP 802.1X.

Why lookaheads and not .*EAP.*TEAP.*802\.1X? Because the non-lookahead version requires the terms in that exact order. Lookaheads are order-independent — each one tests the entire line independently.

Add color + line numbers
grep -rnP --color=always '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/
Count matches per file — ranked by relevance
grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/ | grep -v ':0$' | sort -t: -k2 -rn
Table 7. Pipeline breakdown
Stage What it does

grep -rcP '…​'

-r recursive, -c count per file, -P PCRE. Outputs filename:count for every file.

grep -v ':0$'

Remove files with zero matches. :0$ matches colon + zero + end-of-line.

sort -t: -k2 -rn

-t: split on colon. -k2 sort on second field (the count). -rn reverse numeric (highest first).

Top N files with their matching lines — 5-stage pipeline
grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/ | grep -v ':0$' | sort -t: -k2 -rn | head -3 | cut -d: -f1 | xargs grep -nP --color=always '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)'
Table 8. Pipeline breakdown
Stage What it does

grep -rcP

Count matches per file

grep -v ':0$'

Remove zero-match files

sort -t: -k2 -rn

Rank by count descending

head -3

Take top 3 files

cut -d: -f1

Extract filename only (drop the count)

xargs grep -nP --color=always

Show the actual matching lines from those top 3 files

Each stage is independently testable — run any prefix of the pipeline to see intermediate results. That’s composition.

Variations — different conjunctions
Any 2 of 3 terms (OR within AND)
grep -rnP '(?=.*EAP)(?=.*(?:TEAP|802\.1X))' --include='*.adoc' docs/

(?:…​) is a non-capturing group — groups without creating a backreference. Used here to OR two terms inside a single lookahead.

Case-insensitive
grep -rniP '(?=.*eap)(?=.*teap)(?=.*802\.1x)' --include='*.adoc' docs/
With context (1 line before and after)
grep -rnP -C1 '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/
Negative lookahead — lines with EAP and TEAP but NOT 802.1X
grep -rnP '(?=.*EAP)(?=.*TEAP)(?!.*802\.1X)' --include='*.adoc' docs/

(?!…​) is negative lookahead — asserts the pattern does NOT exist. Inverts the condition for that one term.

Four terms — add RADIUS
grep -rnP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)(?=.*RADIUS)' --include='*.adoc' docs/

Each additional (?=…​) narrows the results. Stack as many as needed.

Search both trees — docs/ AND data/

docs/ holds Antora reference material. data/ holds operational content — investigations, GPO findings, migration projects. Always search both:

Search docs/ (reference)
grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/ | grep -v ':0$' | sort -t: -k2 -rn
Search data/ (operational — d001 projects, investigations)
grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' data/ | grep -v ':0$' | sort -t: -k2 -rn
Search both in one pass
grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/ data/ | grep -v ':0$' | sort -t: -k2 -rn
Top 3 from data/ with matching lines
grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' data/ | grep -v ':0$' | sort -t: -k2 -rn | head -3 | cut -d: -f1 | xargs grep -nP --color=always '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)'
data/d001/ files must be decrypted (d001 open <project>) before grep can see their contents. Encrypted .age files won’t match.
Open top result in nvim
nvim $(grep -rcP '(?=.*EAP)(?=.*TEAP)(?=.*802\.1X)' --include='*.adoc' docs/ | grep -v ':0$' | sort -t: -k2 -rn | head -1 | cut -d: -f1)

Jump to first match inside nvim: /EAP.*TEAP\|TEAP.*EAP

Lookahead quick reference
Syntax Name Matches when

(?=.*X)

Positive lookahead

X exists somewhere ahead

(?!.*X)

Negative lookahead

X does NOT exist ahead

(?⇐X)

Positive lookbehind

X exists immediately before

(?<!X)

Negative lookbehind

X does NOT exist immediately before

(?:X|Y)

Non-capturing group

Groups X or Y without backreference overhead

Lookbehinds in grep -P must be fixed-length — (?⇐EAP) works, (?⇐EAP.*) does not. Lookaheads have no length restriction.
Why this matters

Traditional grep finds one pattern per invocation. Lookaheads let you express "this line must contain A AND B AND C" in a single regex. Without lookaheads, you’d need:

grep 'EAP' file | grep 'TEAP' | grep '802\.1X'

Three processes, three pipes. The lookahead does it in one pass — faster, composable with sort/head/xargs, and the regex itself documents the search intent.

Endpoint Audit — Wired + Wireless (d000 Lab Test)

Endpoint Audit — Wired + Wireless (d000 Lab Test)

Full 3-API audit of both P16g interfaces before running the same workflow against d001 production (5C:FF:35:D8:BB:26) at 1pm.

Prerequisites
dsource d000 dev/network/ise
source data/shared/scripts/ise-helpers.sh
Full endpoint profile — both MACs
Wired — enp134s0 (A8:2B:DD:8F:23:E6)
MAC="A8:2B:DD:8F:23:E6" . $(find data -name 'full-endpoint-profile.sh')
Wireless — wlan0 (E0:D5:5D:6C:E1:66)
MAC="E0:D5:5D:6C:E1:66" . $(find data -name 'full-endpoint-profile.sh')
Comparison — what the APIs returned
Table 9. Key differences between wired and wireless
Field Wired (A8:2B:DD:8F:23:E6) Wireless (E0:D5:5D:6C:E1:66)

ERS Group

Linux-Workstations (static ✅)

Workstation (dynamic — needs fix)

Custom Attributes

OS=Arch Linux, Purpose=Engineering ✅

Empty — needs fix

Description

Evan - modestus-p16g wired - Arch Linux ✅

null — needs fix

NAD

Home-3560CX-01 Gi1/0/2

Home-9800-WLC capwap_90000002

Profiler Policy

Unknown

Linux-Workstation

IP

10.50.1.203

10.50.1.204

Auth Method

dot1x / EAP-TLS / x509_PKI

dot1x / EAP-TLS / x509_PKI

Cert CN

modestus-p16g.inside.domusdigitalis.dev

modestus-p16g.inside.domusdigitalis.dev

MnT Session

null (no active session)

Stop (idle timeout, June 19)

Fix wireless endpoint — match wired config
Write attributes file
jq -n '{OS:"Arch Linux", DevicePurpose:"Engineering Workstation"}' > /tmp/ep-attrs.json
Update wireless endpoint — Linux-Workstations group
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 wireless - Arch Linux" /tmp/ep-attrs.json
Styled result check
jq -r '
  def none: "∅";
  "ISE ERS Endpoint Update Result",
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
  (.UpdatedFieldsList.updatedField[] |
    "✓ \(.field)\n  old: \(.oldValue // none)\n  new: \(.newValue // none)\n"
  )
' /tmp/endpoint-E0D55D6CE166-result.json
Tabular result
{
  printf 'FIELD\tOLD\tNEW\n'
  jq -r '
    .UpdatedFieldsList.updatedField[]
    | [.field, (.oldValue // "∅"), (.newValue // "∅")]
    | @tsv
  ' /tmp/endpoint-E0D55D6CE166-result.json
} | column -t -s $'\t'
Verify it stuck
bash $(find data/d000/infra/ise-api-reference/scripts/ers -name 'update-endpoint.sh') E0:D5:5D:6C:E1:66
DataConnect — individual queries (practice for d001)
Auth method breakdown — both MACs
dc_query "
SELECT CALLING_STATION_ID AS MAC, 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 IN ('A8:2B:DD:8F:23:E6','E0:D5:5D:6C:E1:66')
GROUP BY CALLING_STATION_ID, AUTHENTICATION_METHOD, AUTHENTICATION_PROTOCOL, CREDENTIAL_CHECK
ORDER BY CALLING_STATION_ID, CNT DESC
" | jq '.'
Timeline — first/last seen for both
dc_query "
SELECT CALLING_STATION_ID AS MAC,
       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 IN ('A8:2B:DD:8F:23:E6','E0:D5:5D:6C:E1:66')
GROUP BY CALLING_STATION_ID
" | jq '.'
NAD history — which devices have these MACs connected to
dc_query "
SELECT CALLING_STATION_ID AS MAC, NAS_IP_ADDRESS, DEVICE_NAME, NAS_PORT_ID, NAS_PORT_TYPE
FROM radius_authentications
WHERE CALLING_STATION_ID IN ('A8:2B:DD:8F:23:E6','E0:D5:5D:6C:E1:66')
ORDER BY CALLING_STATION_ID, TIMESTAMP_TIMEZONE DESC
" | jq '.'
Failure analysis — any failed auths for either MAC (empty array = zero failures = good)
dc_query "
SELECT CALLING_STATION_ID AS MAC, TIMESTAMP_TIMEZONE, FAILURE_REASON,
       AUTHENTICATION_METHOD, POLICY_SET_NAME, DEVICE_NAME
FROM radius_authentications
WHERE CALLING_STATION_ID IN ('A8:2B:DD:8F:23:E6','E0:D5:5D:6C:E1:66')
  AND FAILED > 0
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 20 ROWS ONLY
" | jq '.'
Accounting — discover column names first (d000 columns may differ from d001)
dc_query "SELECT * FROM radius_accounting FETCH FIRST 1 ROWS ONLY" | jq '.[0] | keys'
Accounting — session time, bytes for both (uses CALLING_STATION_ID not MAC_ADDRESS)
dc_query "
SELECT CALLING_STATION_ID AS MAC, USERNAME, ACCT_SESSION_TIME, ACCT_STATUS_TYPE,
       ACCT_INPUT_OCTETS AS BYTES_IN, ACCT_OUTPUT_OCTETS AS BYTES_OUT,
       ACCT_TERMINATE_CAUSE
FROM radius_accounting
WHERE CALLING_STATION_ID IN ('A8:2B:DD:8F:23:E6','E0:D5:5D:6C:E1:66')
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 10 ROWS ONLY
" | jq '.'
Styled timeline output
dc_query "
SELECT CALLING_STATION_ID AS MAC, TIMESTAMP_TIMEZONE, USERNAME,
       AUTHENTICATION_PROTOCOL, PASSED, DEVICE_NAME
FROM radius_authentications
WHERE CALLING_STATION_ID IN ('A8:2B:DD:8F:23:E6','E0:D5:5D:6C:E1:66')
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 10 ROWS ONLY
" | jq -r '
  "Auth Timeline — Both Interfaces",
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
  (.[] |
    "\(.TIMESTAMP_TIMEZONE)  \(.MAC)  \(.PASSED)  \(.AUTHENTICATION_PROTOCOL)  \(.DEVICE_NAME)"
  )
'
Ready for d001

Same commands work at d001 — replace dsource d000 with dsource d001 and swap the MACs for 5C:FF:35:D8:BB:26. All queries use IN (…​) syntax — add more MACs by appending to the list.

Abnormal Security — Phase 3 All IS (Live Execution 2026-06-25)

Phase 3 executed live by Evan at 14:30. Dynamic pull from ISAllHands distro group. Cleaned version in d001 project: partials/phase3-execution-2026-06-25.adoc. Raw execution log: notes/itrack-unsorted-chg-adlib-adoc-2026-06-25.adoc.

d001 open abnormal-security
nvim $(find data/d001/projects/abnormal-security -type f -regex '.*phase3-execution.*\.adoc')

External Email Tag Stacking — Transport Rule Investigation

Reported via Teams by Sarah Clizer. ExceptIfSubjectContainsWords missing on external sender modification rule. Investigation commands in d001 project.

d001 open abnormal-security
nvim $(find data/d001/projects/abnormal-security -type f -regex '.*external-tag.*\.adoc')

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.


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/'