WRKLOG-2026-06-28
Summary
Sunday. 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 |
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-repoinstalled -
Expressions file reviewed — no false positives (e.g., Don Quijote "Angulo el Malo" is in
segunda-parte/texto/texto-011.adoc— the regex targetsangulo-arreolaspecifically 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: |
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: |
TBD |
In Progress |
Financial discovery — FL-142 preparation |
Gopass Security Audit |
Rotate passwords on shared/known accounts. Add 2FA backup codes to |
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 |
|---|---|---|
AppArmor git/gopass EACCES (INC-003) — RESOLVED |
Done |
comms-tshoot-apparmor-git-gopass-2026-06-27.adoc — Case 001 closed |
Faillock sudo lockout |
Done |
apparmor-gopass-faillock/faillock-incident.adoc |
ISE MnT diagnostics via sshpass |
Done |
ise-mnt-diagnostics.adoc — findings captured, restart pending |
wpa_supplicant fix + MAC discovery |
Done |
wpa-supplicant-mac-discovery.adoc — INC-002 chain resolved |
search-endpoint.sh operationalized |
Done |
ise-api-search-endpoint.adoc + dataconnect-overview.adoc |
ers_all jq aggregation bug |
Done |
ise-common.sh — --slurpfile → jq -s |
SSH 3560CX legacy crypto |
Pending |
ssh-config-fixes.adoc — 00-exceptions.conf patch ready |
SSH modestus-p50 missing host |
Pending |
ssh-config-fixes.adoc — need IP for 80-d000-hosts.conf |
gopass YAML for switch |
Pending |
/tmp/gopass-3560cx-01.yaml — template ready for gopass edit |
apparmor-gopass-faillock
AppArmor Git/Gopass EACCES + Faillock Sudo Lockout
Two linked incidents during the ISE MnT diagnostics session. The AppArmor git deny on gopass stores caused repeated sudo failures, which triggered faillock and locked sudo entirely.
Faillock Sudo Lockout
Sudo stopped accepting password input — sudo: a password is required returned immediately with no prompt, or the prompt appeared but Enter had no effect.
Symptoms
-
sudo ls→sudo: a password is required(no prompt) -
sudo ls→ prompt appears, password typed, Enter pressed → rejected -
Terminal
sttyshowed-echoe -echok(unusual echo state)
Diagnosis
faillock --user evanusmodestus
evanusmodestus:
When Type Source Valid
2026-06-27 23:54:26 TTY /dev/pts/24 V
2026-06-27 23:42:50 TTY /dev/pts/24 I
2026-06-27 23:43:19 TTY /dev/pts/24 I
Failed sudo attempts triggered pam_faillock. Lockout threshold reached.
Fix
su
faillock --user evanusmodestus --reset
exit
sudo ls
su-
Escalate via root password — faillock only blocks PAM-authenticated sudo, not
su faillock --reset-
Clears the failure counter
sudo ls-
Confirm sudo works again
Wazuh Evidence
Wazuh rule 80794 fired at 06:54:28 UTC — the unlock event:
Level 8 — Rule 80794
Audit: Passwd was used to unlock an account.
Agent: modestus-p16g
Time: 2026-06-28T06:54:28.276+0000
Wazuh Gap
Wazuh logged the unlock (rule 80794) but NOT the lockout itself. The failed sudo attempts that triggered faillock were not captured as alerts. Needs a custom decoder/rule for pam_faillock lockout events.
Lessons
-
sudo: a password is requiredwith no prompt = faillock lockout, not a password problem -
subypasses faillock — use it to reset -
Check
faillock --user <name>before assuming password is wrong -
Wazuh needs a custom rule for
pam_faillocklockout events — currently blind to this
AppArmor Git Profile — Gopass Store Fix
INC-003: AppArmor’s /usr/bin/git profile has an explicit deny @{HOME}/.local/share/gopass/** r, that blocks git from reading .git/HEAD in gopass stores. gopass sync fails, git reports "not a git repository."
Root Cause — strace Proof
strace -e openat git -C ~/.local/share/gopass/stores/v2 status 2>&1 \
| grep -i '.git\|EACCES'
openat(AT_FDCWD, "...gopass/stores/v2/.git/HEAD", O_RDONLY) = -1 EACCES (Permission denied)
The Offending Rule
# In /etc/apparmor.d/usr.bin.git:
deny @\{HOME}/.local/share/gopass/** r, ← BLOCKS git from reading .git/HEAD
owner @\{HOME}/** rw, ← this ALLOW is overridden by the deny
AppArmor deny is absolute — overrides any allow regardless of order.
Fix — Before
sudo grep -n 'gopass' /etc/apparmor.d/usr.bin.git
Fix — Apply
sudo cp /etc/apparmor.d/usr.bin.git /etc/apparmor.d/usr.bin.git.bak.$(date +%F)
sudo sed -i 's| deny @\{HOME}/\.local/share/gopass/\*\* r,| # Gopass stores — git manages these as repos (sync, pull, push)\n # Deny reading encrypted secrets but allow .git operations\n deny @\{HOME}/.local/share/gopass/stores/*/*.gpg r,\n owner @\{HOME}/.local/share/gopass/stores/** rwkl,|' /etc/apparmor.d/usr.bin.git
sudo apparmor_parser -R /etc/apparmor.d/usr.bin.git && \
sudo apparmor_parser -a /etc/apparmor.d/usr.bin.git
Fix — After
sudo grep -n 'gopass' /etc/apparmor.d/usr.bin.git
strace -e openat git -C ~/.local/share/gopass/stores/v2 status 2>&1 | grep '.git/HEAD'
gopass sync
sshpass -p "$(gopass show v3/domains/d000/identity/ise/ise-01 | yq -r '.credentials.cli.password')" \
ssh ise-01 'show application status ise' 2>&1 \
| tee /tmp/ise-app-status-$(date +%F-%H%M).txt
.git/HEAD must return a file descriptor (e.g., = 3), not EACCES.
Pattern — Third AppArmor EACCES Incident
| Incident | Blocked Path | Fix |
|---|---|---|
INC-001 (curl) |
|
|
INC-003 (git) |
|
Replace blanket deny with surgical |
(Future sshpass) |
TBD |
Check after git fix |
The universal diagnostic: strace -e openat <command> 2>&1 | grep EACCES
ise-mnt-diagnostics
ISE MnT Diagnostics — sshpass Pipeline
First successful remote ISE CLI diagnostics session using the sshpass + gopass pipeline. The AppArmor git/gopass fix from earlier today unblocked gopass sync, which unblocked gopass show, which unblocked the sshpass pipeline.
Pipeline
PASS=$(gopass show v3/domains/d000/identity/ise/ise-01 | yq -r '.credentials.cli.password') (1)
sshpass -p "$PASS" ssh ise-01 \ (2)
'screen-length 0 ; show application status ise' \ (3)
2>&1 | tee /tmp/ise-app-status-$(date +%F-%H%M).txt (4)
unset PASS (5)
| 1 | Extract CLI password from gopass YAML — tail -n +2 is implicit in yq since we pipe the full gopass output. The YAML starts after line 1 (the bare password line). |
| 2 | sshpass feeds the password non-interactively. ISE ADE-OS doesn’t support key-based auth — password is the only option. |
| 3 | screen-length 0 disables the ISE pager. This is NOT terminal length 0 (IOS syntax) — ISE ADE-OS uses different CLI commands. Without this, --More-- truncates output and the session hangs. |
| 4 | 2>&1 merges stderr (SSH banners, warnings) into stdout. tee writes to file while displaying — timestamped for the record. |
| 5 | Credential hygiene — clear the password from the shell environment immediately after use. Prevents exposure via /proc/$$/environ or env. |
Findings from /tmp/ise-diagnostics-2026-06-28-0008.txt
| Log | Finding | Severity |
|---|---|---|
|
Normal — SSH login accepted, MnT alarm tasks running, portal ports active |
OK |
|
GC Event queue full — |
WARNING — JVM memory pressure |
|
|
WARNING — cert missing Key Usage extension |
|
|
WARNING — session cache miss, related to MnT collection gap |
|
Single AD diagnostic entry — quiet |
OK |
|
|
COSMETIC — noisy but harmless |
|
|
OK |
|
Total 16GB, used 12GB, available 2GB, swap 1.1GB/9.2GB |
WARNING — ISE is swapping, memory-constrained |
Conclusion
All ISE services report running. Memory pressure (12GB/16GB, swapping) combined with GC queue overflow suggests MnT collection may be failing intermittently under load. The session cache miss for A0:59:50:C9:92:EE confirms ISE is dropping sessions. Next step: restart ISE application to clear JVM memory pressure, then verify MnT collection resumes.
wpa-supplicant-mac-discovery
wpa_supplicant Fix Chain + MAC Discovery
The root cause of 20+ days of missing wired 802.1X sessions: wpa_supplicant-wired@enp134s0 was not running. Fixing this revealed the correct wired MAC address for the P16g.
Fix Applied
sudo systemctl enable --now wpa_supplicant-wired@enp134s0 (1)
| 1 | enable --now does two things: creates the symlink for boot persistence AND starts the service immediately. Without --now, enable alone only affects next boot — the supplicant stays dead until reboot. This single missing --now was the root cause of 20 days of missing wired sessions. |
The config also had eap=TEAP — unsupported by the compiled wpa_supplicant binary on Arch. Changed to eap=TLS.
MAC Address Discovery via search-endpoint.sh
Used the new search-endpoint.sh DataConnect tool to confirm the P16g’s identity in ISE:
bash $(find data -name 'search-endpoint.sh' -path '*/dataconnect/*') p16g (1)
| 1 | $(find …) locates the script by name regardless of working directory — portable across machines. The argument p16g searches radius_authentications.username (EAP-TLS certificate CN), endpoints_data.hostname, and calling_station_id (MAC) via LIKE '%p16g%'. |
| MAC | Interface | NAS Port | Last Auth |
|---|---|---|---|
|
wlan0 (wireless) |
|
2026-06-08 |
|
enp134s0 (wired) |
|
2026-06-07 |
Confirmed Wired MAC
ip link show enp134s0 | awk '/ether/{print $2}' (1)
# a8:2b:dd:8f:23:e6
| 1 | awk '/ether/' matches the line containing the MAC address. {print $2} extracts the second field — the MAC itself. Faster than grep -o with a regex pattern, and the output is clean (no surrounding text). |
This is NOT 14:F6:D8:7B:31:80 (previously assumed wired MAC in SSH config and Antora attributes). The 14:F6:D8:7B:31:80 MAC is the Thunderbolt/USB ethernet adapter — only used when docked. A8:2B:DD:8F:23:E6 is the built-in Intel NIC.
Impact
| Item | Action |
|---|---|
|
Needs updating: |
ISE endpoint |
Both MACs exist in ISE endpoint table — both authenticate via EAP-TLS with CN |
gopass ISE entry |
|
SSH config |
|
ise-api-search-endpoint
ISE API — search-endpoint.sh Operationalized
Built and validated search-endpoint.sh — a DataConnect script that searches for any endpoint by hostname, EAP-TLS certificate CN, or MAC address pattern. Searches radius_authentications joined with endpoints_data.
Why ERS Can’t Do This
ERS endpoint names are MAC addresses. Searching by hostname (p50, vocera, timeclock) returns zero results. DataConnect stores the EAP-TLS certificate CN in radius_authentications.username and DHCP hostname in endpoints_data.hostname — both searchable.
Usage
dsource d000 dev/network/ise (1)
bash $(find data -name 'search-endpoint.sh' -path '*/dataconnect/*') p16g (2)
bash $(find data -name 'search-endpoint.sh' -path '*/dataconnect/*') p50 (3)
bash $(find data -name 'search-endpoint.sh' -path '*/dataconnect/*') 14:F6:D8 (4)
dsource d001 dev/network/ise (5)
bash $(find data -name 'search-endpoint.sh' -path '*/dataconnect/*') vocera (6)
| 1 | dsource loads ISE API credentials into the shell environment — ISE_PAN_FQDN, ISE_API_USER, ISE_API_PASS, ISE_CA_CERT, ISE_DATACONNECT_*. The d000 argument selects the home lab; d001 selects production. |
| 2 | Search by hostname substring — matches radius_authentications.username where EAP-TLS stores the certificate CN (e.g., modestus-p16g.inside.domusdigitalis.dev). |
| 3 | Same pattern, different host — found P50 with MAC 14:F6:D8:7B:31:80, last auth 2026-06-13 via wireless. |
| 4 | Partial MAC search — LIKE '%14:F6:D8%' matches any endpoint with this OUI prefix. |
| 5 | Switching to production ISE — the exact same script, different credentials. dc_query connects to the d001 DataConnect instance. |
| 6 | At work: "Hey Evan, are the Vocera phones authenticating?" — one command, instant answer with MAC, IP, profile, NAS port, auth protocol, pass/fail, and timestamp. |
ers_all Fix
ers_all() in ise-common.sh had a jq bug — --slurpfile loaded files into variable $pages but the expression referenced .pages[] (a field path). All paginated ERS calls returned invalid JSON. Fixed to jq -s with stdin slurp. Added empty-result guard for --argjson total when API returns non-JSON.
Files Created/Modified
-
scripts/dataconnect/search-endpoint.sh— new -
scripts/dataconnect/queries/search-endpoint.sql— new -
partials/dataconnect-overview.adoc— wired in with collapsible, validated output, usage examples -
scripts/lib/ise-common.sh—ers_allfix + empty-result guard
Gopass Switch Entry
Created YAML template at /tmp/gopass-3560cx-01.yaml for the lab switch — credentials for SSH, enable, console, API, SNMP, RADIUS, and TACACS with _use: keys per gopass YAML standard from the 2026-06-24 audit.
Path: v3/domains/d000/network/switch/3560cx-01
ise-health-check-script
ISE health-check.sh — Post-Restart Validation
Built health-check.sh to validate all 5 ISE API surfaces in a single command. Environment-agnostic via dsource — works against d000 and d001.
What It Checks
| API | Check | Metric |
|---|---|---|
ERS (9060) |
|
Total endpoint count |
MnT (443) |
|
Active session count (XML → xq) |
OpenAPI (443) |
|
Policy set count |
DataConnect (2484) |
|
Auths in last hour |
pxGrid (8910) |
|
Account state (ENABLED/DISABLED) |
Plus system info (hostname, FQDN, version, patch, roles, services) and DataConnect summary stats (total auths, 24h auths, 24h failures, unique MACs).
Iteration Log
| Issue | Fix |
|---|---|
|
Removed |
MnT empty response |
Added guard: if response is empty, print "MnT not ready" instead of crashing xq |
OpenAPI missing |
|
Gopass path in public script |
Removed CLI diagnostics block — credential paths don’t belong in shareable scripts |
Version/patch showing "—" |
ERS |
sshpass hang |
Removed CLI diagnostics entirely — sshpass blocks on GPG passphrase or SSH timeout |
Usage
dsource d000 dev/network/ise (1)
bash data/d000/infra/ise-api-reference/scripts/health-check.sh (2)
| 1 | Loads all ISE credentials into environment — ERS, MnT, OpenAPI, DataConnect, and pxGrid. The health check script sources ise-common.sh which validates these variables are set before any API call. |
| 2 | Checks all 5 surfaces sequentially. Each check uses check_api() — a function that captures the command output, prints ✓ on success or ✗ FAILED with the error message. Output auto-saved to /tmp/ise-health-check-YYYY-MM-DD-HHMM.txt. |
Documented in partials/cross-api-patterns.adoc with collapsible source and validated output sample.
ssh-config-fixes
SSH config.d Fixes
Two SSH connectivity issues discovered during switch diagnostics.
Issue 1 — 3560CX Legacy Crypto
ssh 3560cx-01 fails:
Unable to negotiate with 10.50.1.10 port 22: no matching host key type found. Their offer: ssh-rsa
Root cause: 30-cisco-defaults.conf matches 3560cx-* with HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256 — but the 3560CX runs classic IOS (not IOS-XE) and only offers ssh-rsa. Same for KEX — only diffie-hellman-group14-sha1.
Fix: Add legacy exception in 00-exceptions.conf (loads before 30-cisco-defaults.conf — SSH uses first match):
Host 3560cx-01 (1)
User admin
PubkeyAuthentication no (2)
PasswordAuthentication yes
KbdInteractiveAuthentication yes
PreferredAuthentications keyboard-interactive,password
KexAlgorithms +diffie-hellman-group14-sha1 (3)
HostKeyAlgorithms +ssh-rsa (4)
PubkeyAcceptedAlgorithms +ssh-rsa
Ciphers aes256-ctr,aes128-ctr (5)
MACs hmac-sha2-256,hmac-sha1
ControlMaster no (6)
ConnectTimeout 10
| 1 | Exact hostname match in 00-exceptions.conf — wins over the wildcard 3560cx-* in 30-cisco-defaults.conf because SSH uses first match (lexicographic file order). |
| 2 | Classic IOS has no SSH key auth — disable pubkey to avoid timeout delays probing unsupported algorithms. |
| 3 | + prefix appends to (not replaces) the system default KEX list. diffie-hellman-group14-sha1 is the only KEX the 3560CX supports — without it, negotiation fails before authentication. |
| 4 | +ssh-rsa appends the legacy host key algorithm. Modern OpenSSH disabled ssh-rsa by default (SHA-1 signatures). The 3560CX offers nothing else — this is the actual error: "no matching host key type found." |
| 5 | AES-CTR only — the 3560CX doesn’t support GCM ciphers. aes256-ctr is the strongest option available. |
| 6 | ControlMaster no prevents SSH multiplexing — IOS doesn’t handle shared connections correctly. Each session must be independent. |
Status: Pending — user to apply and re-encrypt.
Issue 2 — modestus-p50 Missing Host Entry
ssh modestus-p50 fails:
ssh: Could not resolve hostname modestus-p50: Name or service not known
Root cause: No Host modestus-p50 entry in 80-d000-hosts.conf. The hostname exists in DNS but SSH config doesn’t have a mapping.
Fix: Add to 80-d000-hosts.conf:
Host modestus-p50
HostName <IP — resolve via dig or ISE DataConnect search>
Used search-endpoint.sh p50 to find the P50 in ISE: MAC 14:F6:D8:7B:31:80, CN p50.inside.domusdigitalis.dev, last auth via wireless on 2026-06-13.
Status: Pending — need IP address (DHCP or static reservation).
Post-fix
age -e -R ~/.age/recipients/self.txt \ (1)
-o ~/.ssh/config.d/80-d000-hosts.conf.age \ (2)
~/.ssh/config.d/80-d000-hosts.conf (3)
| 1 | -R specifies the recipients file — the public key(s) that can decrypt. Using a file avoids embedding keys on the command line. |
| 2 | -o sets the output path — the encrypted .age file that gets tracked in git. The plaintext source is gitignored. |
| 3 | The plaintext input file. After encrypting, this stays on disk (protected by .gitignore) for stow to symlink into ~/.ssh/config.d/. |
cpu-temp-runaway-less
CPU Temperature Spike — Runaway less Process
Symptoms
-
Package temp 93°C (crit 105°C)
-
Core 28 at 94°C, Core 44 at 84°C — asymmetric spike
-
Load average 2.4 on 24 cores — not high, but sustained
-
No obvious userspace CPU consumer in
ps aux
Diagnostic
sensors | head -30 (1)
top -bn1 | head -15 (2)
| 1 | sensors reads coretemp-isa-0000 — the kernel’s hardware monitoring interface. Each core reports independently. Package temp is the maximum across all cores — the thermal throttle trigger. |
| 2 | top -bn1 — batch mode (-b), single iteration (-n1). Without -b, top takes over the terminal. The output is sorted by CPU% by default — the first non-header line is the culprit. |
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3383353 evanusm+ 20 0 7180 4952 4568 R 100.0 0.0 201:17.31 less
1243951 evanusm+ 20 0 3171916 1.7g 100048 S 28.6 2.7 48:54.30 claude
Root Cause
PID 3383353: less -R -F -K reading from a disconnected pipe. The source process exited but less kept spinning at 100% on a single core for 201 minutes (3.3 hours). The -F flag (quit if one screen) doesn’t trigger when stdin is a pipe that’s been closed — less enters a busy-wait loop.
cat /proc/3383353/cmdline | tr '\0' ' ' (1)
# less -R -F -K
ls -l /proc/3383353/fd/0 (2)
# pipe:[5994563] ← orphaned pipe, source gone
| 1 | /proc/<pid>/cmdline stores the full command line with NUL (\0) delimiters between arguments — not spaces. tr '\0' ' ' makes it human-readable. This is how you recover the exact invocation of any running process. |
| 2 | /proc/<pid>/fd/0 is stdin. The symlink target pipe:[5994563] is a kernel pipe inode — the number is the inode ID. When the writing process exits, the pipe becomes orphaned but less doesn’t detect EOF and busy-waits instead. |
Fix
kill 3383353
Prevention
This pattern recurs with pagers attached to pipes. The -F + -K flags on less are common in $PAGER / $MANPAGER / git config. When the pipe source exits uncleanly (Ctrl+C, broken SSH, killed process), less can spin indefinitely.
Options:
-
Set
LESS="-R -F -X -K --quit-if-one-screen"— the--quit-if-one-screenvariant handles edge cases better -
Periodic check:
ps aux | awk '$3 > 50 && /less/{print}'— catches runaway pagers -
tmux/terminal alert on sustained high CPU per process
cli-search-proficiency
CLI Search Proficiency — Querying Your Own System
The entire documentation system is searchable text. grep -rn 'bash test' docs/modules returned nothing because the prose "bash test" doesn’t appear — the actual syntax is [[ ]]. The gap isn’t tooling, it’s knowing how to ask the question in each tool’s language.
The entire documentation system is searchable text — AsciiDoc, YAML, shell scripts, SQL, config files. Every tool you need is already on the machine. The gap isn’t tooling, it’s fluency — knowing which tool to reach for and how to compose them.
grep — Pattern Search (the foundation)
grep -rn '\[\[' docs/modules/ROOT/partials/ (1)
grep -rn '\[\[.*\]\]' docs/modules/ROOT/partials/ --include='*.adoc' (2)
grep -rnP '(?<=\[\[).*?(?=\]\])' docs/modules/ROOT/ --include='*.adoc' (3)
grep -rl 'strace' docs/ data/ (4)
grep -rnC2 'EACCES' docs/modules/ROOT/partials/standards/ (5)
grep -rn 'source,bash' docs/ --include='*.adoc' | wc -l (6)
grep -rnv '^//' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28.adoc (7)
| 1 | -rn — recursive + line numbers. The bare minimum for any search. Find bash test [[ ]] syntax across all partials. |
| 2 | --include='*.adoc' — restrict to AsciiDoc files. Without this, grep hits .json, .sh, .yaml — noise when you want documentation. |
| 3 | -P — PCRE (Perl-compatible regex). Lookahead (?⇐\[\[) and lookbehind (?=\]\]) extract content inside [[ ]] without the brackets themselves. This is the grep that couldn’t find your bash tests — because bash test is prose, the actual syntax is [[ ]]. |
| 4 | -rl — recursive, files-only. "Which files mention strace?" — paths only, no line content. Fastest way to scope a search. |
| 5 | -C2 — 2 lines of context above and below each match. When you need to see the section heading that contains the match. |
| 6 | Pipe to wc -l — counting, not reading. "How many code blocks do I have?" Answer: a number, not a wall of text. |
| 7 | -v — invert match. Lines that are NOT comments. See the actual content of an assembler without the // headers. |
find — File Discovery (the navigator)
find docs/ -name '*.adoc' -newer docs/antora.yml (1)
find data/ -name '*.sh' -executable (2)
find docs/modules/ROOT/partials/worklog/daily-notes/ -type d -name '2026-06-*' (3)
find . -name '*.adoc' -path '*/standards/*' (4)
find data/ -name '*.sql' -exec head -2 {} + (5)
find docs/ -name '*.adoc' -size +50k (6)
find . -name '*.adoc' -newer /tmp/ise-health-check-2026-06-28-1017.txt (7)
| 1 | -newer — files modified after a reference file. "What changed since I last touched antora.yml?" Time-based triage without git. |
| 2 | -executable — only scripts with the execute bit. Filters out .sh files that are source includes (not runnable). |
| 3 | -type d — directories only. "Which days have daily notes this month?" One line, instant answer. |
| 4 | -path '/standards/' — glob on the full path. More precise than -name when files share names across directories. |
| 5 | -exec head -2 {} + — run head on every match, batched. Read the first two lines of every SQL file — the comment header tells you what each query does without opening them. |
| 6 | -size +50k — files over 50KB. Find monolithic partials that violate the "split when large" principle. |
| 7 | -newer with a /tmp timestamp file — "what .adoc files did I touch since the health check?" Uses any file as a time reference. |
awk — Field Extraction (the surgeon)
awk '/^=====/{print FILENAME": "$0}' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28/*/*.adoc (1)
awk '/^\| Task/,/^\|===/' data/d000/infra/ise-api-reference/partials/cross-api-patterns.adoc | awk '/^\|/{gsub(/^\| */,""); print}' (2)
awk '/^\[source,bash\]/{p=1;next} p&&/^----/{if(++d==1)next; p=0;d=0;next} p&&d' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28/ise-mnt-diagnostics/sshpass-pipeline.adoc (3)
awk '/^include::/{gsub(/include::|partial\$|\[\]/,""); print}' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28.adoc (4)
awk 'FNR==1{print "--- "FILENAME" ---"} /TODO|FIXME|HACK/{print FNR": "$0}' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28/*/*.adoc (5)
| 1 | Pattern match on ===== (h5 headings) — extract every section title from today’s sub-partials with the filename prefix. Instant table of contents. |
| 2 | Two-stage awk pipeline. First awk uses range pattern /^\| Task/,/^\|===/ to extract the table between the header row and the closing delimiter. Second awk strips the leading | ` from each cell. AsciiDoc multi-line tables put each cell on its own `| value line — -F'|' doesn’t work because there’s only one | per line. |
| 3 | State machine: p=1 arms on [source,bash], d counts ---- delimiters (first opens, second closes), content prints only when p&&d are both true. Cleaner than range patterns which leak the delimiter lines. |
| 4 | gsub — global substitution. Strip include::, partial$, and [] from include directives to get bare file paths. Shows the dependency tree of an assembler. |
| 5 | FNR==1 — first line of each file (multi-file aware). Print a header per file, then any TODO/FIXME/HACK lines with line numbers. Code audit in one pass. |
sed — In-Place Transformation (the editor)
sed -n '/^====/,/^====/p' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28/cpu-temp-runaway-less/investigation.adoc (1)
sed -n '10,25p' data/d000/infra/ise-api-reference/scripts/lib/ise-common.sh (2)
sed '/^\/\//d' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28/ise-mnt-diagnostics.adoc (3)
sed -n 's/.*include::\(.*\)\[\].*/\1/p' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28.adoc (4)
| 1 | Range address /start/,/end/ with -n and p — print only the first section (between two ==== headings). Non-destructive extraction. |
| 2 | Line range 10,25p — print lines 10-25. When you know the line numbers from a grep -n result and want context. Faster than opening in an editor. |
| 3 | /^\/\//d — delete comment lines. View the rendered content of any AsciiDoc file without the // metadata headers. |
| 4 | Capture group \(…\) — extract the path from every include:: directive. Same as the awk version but using sed’s substitution with backreferences. |
Composition — Multi-Tool Pipelines
grep -rl 'AppArmor' docs/ | xargs grep -l 'EACCES' (1)
find data/ -name '*.sh' | xargs head -5 | grep -B1 'Author' (2)
grep -rn 'dc_query' data/d000/infra/ise-api-reference/scripts/ | awk -F: '{print $1}' | sort -u (3)
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-28/ -name '*.adoc' -exec grep -l 'callout\|<1>' {} + (4)
grep -rn '\[source,' docs/ --include='*.adoc' | awk -F, '{print $2}' | tr -d ']' | sort | uniq -c | sort -rn (5)
| 1 | Two-stage grep — first find files mentioning AppArmor, then narrow to those that also mention EACCES. Intersection search. |
| 2 | find | xargs head | grep -B1 — three-stage pipeline. Find all scripts, read their first 5 lines, filter for Author metadata with one line of context above (the filename separator from head). |
| 3 | awk -F: '{print $1}' | sort -u — extract unique filenames from grep output. "Which scripts use dc_query?" — deduplicated file list. |
| 4 | find -exec grep -l — find files containing callout markers. Verify which sub-partials have callouts and which still need them. |
| 5 | Language census — count every [source,<lang>] block across all docs, sort by frequency. "How much bash vs SQL vs yaml do I have?" One pipeline. |
The Missing Piece — Terminal-Native Idea Capture
You have 19 repos, 48 projects, 19,479 bash code blocks, and a four-layer documentation standard. You have discoveries (STD-019), daily triage tables, project inboxes, cogitatio files, and reMarkable PDF exports. What you don’t have is a zero-friction command that captures a thought the instant it forms and routes it to the right place later.
The idea isn’t silly. It’s the capstone of everything you’ve built.
What Already Exists
| System | Where It Lives | Gap |
|---|---|---|
Discoveries (STD-019) |
|
Requires knowing the category at capture time — too much friction for raw ideas |
Daily triage table |
|
Right paradigm ("write here first, distribute later") but buried inside the worklog shell |
Project inbox |
|
Per-project — useless when the idea doesn’t have a project yet |
Cogitatio |
|
Domain-organized personal capture — closest to what you want, but age-encrypted and not linked to the CLI |
reMarkable ideas |
|
Analog capture — great for offline, but not searchable or linkable |
What You Actually Want
A command that does this:
idea "AppArmor profiles should be version-controlled in git" (1)
idea -p ise-api "add certificate expiry check to health-check.sh" (2)
idea -t education "read chapter 7 of K&R before Thursday" (3)
idea --review (4)
idea --search "apparmor" (5)
| 1 | Bare idea — no category, no project, no ceremony. Timestamped, appended to today’s inbox, searchable immediately. The thought takes 3 seconds to capture instead of dying in your frontal lobe. |
| 2 | -p routes to a project’s inbox partial. If the project exists, append. If not, create the inbox. |
| 3 | -t tags with a category for later triage — education, infrastructure, work, personal, project-idea. |
| 4 | --review opens today’s captured ideas for triage — move to discoveries, link to projects, or archive to graveyard. |
| 5 | --search runs grep -rn across all idea files. Your entire ideation history, searchable from the terminal. |
Architecture
~/.local/bin/idea ← shell function or script
│
├── Default: append to today's idea file
│ docs/modules/ROOT/partials/worklog/daily-notes/YYYY-MM-DD/ideas/raw.adoc
│
├── -p <project>: append to project inbox
│ data/<tier>/projects/<slug>/partials/inbox.adoc
│
├── -t <tag>: append with category tag
│ Same file, but prefixed with [tag] for triage filtering
│
├── --review: open today's ideas in $EDITOR
│ nvim $(find docs/modules/ROOT/partials/worklog/daily-notes/$(date +%F) -name 'raw.adoc')
│
└── --search <term>: grep all idea files
grep -rn <term> docs/modules/ROOT/partials/worklog/daily-notes/*/ideas/
Storage Format
Each idea is one entry in today’s ideas/raw.adoc sub-partial:
===== 2026-06-28 10:45 — AppArmor profiles should be version-controlled
[tags: infrastructure, apparmor]
AppArmor profiles should be version-controlled in git. Currently editing
/etc/apparmor.d/ directly with sudo sed — no history, no rollback, no
diff. The curl and git profile fixes from this week would have been
safer with a git-managed workflow.
**Status:** raw +
**Disposition:** _pending triage_
---
Lifecycle — From Thought to Artifact
Captured (raw) ← idea "thought"
│
├── Triage (daily/weekly review) ← idea --review
│ │
│ ├── → Graveyard ← not worth pursuing, archived with reason
│ │ docs/modules/ROOT/partials/worklog/ideas/graveyard.adoc
│ │
│ ├── → Discovery (STD-019) ← micro-learning, append to discoveries/<cat>/
│ │ Graduation: 3+ references → Codex
│ │
│ ├── → Existing Project ← link to project inbox or backlog
│ │ data/<tier>/projects/<slug>/partials/inbox.adoc
│ │
│ └── → New Project ← idea becomes PRJ-YYYY-MM-<slug>
│ data/project-master.yaml + full project scaffold
│
└── Search (anytime) ← idea --search "term"
grep across all raw + triaged idea files
Triage Categories
| Disposition | Where It Goes | Trigger |
|---|---|---|
Graveyard |
|
Not actionable, already solved, or low value — archived with reason so you don’t re-think it |
Discovery |
|
A technique, tool, or insight worth remembering — graduates to Codex at 3+ references |
Project backlog |
|
Actionable and belongs to an existing project — becomes a task or phase |
New project |
|
Big enough to be its own project — gets PRJ ID, metadata, summary, phases |
Concern partial |
|
Worth documenting today — becomes a daily note concern with sub-partials |
Standard update |
|
Policy-level insight — update an existing standard or propose a new one |
Why This Matters
Every system you’ve built — ISE API reference, systems-debugging curriculum, AppArmor troubleshooting thoroughfares, the four-layer worklog architecture — started as a thought. The ones that made it into artifacts are the ones that happened to occur while you were already at the keyboard with a file open. The ones that died are the ones that hit while you were in a meeting, on a call, walking to the kitchen, or debugging something else.
The idea command closes that gap. Three seconds from thought to searchable text. Zero friction. The triage happens later, when you have context and calm — not in the moment of inspiration when you’re already deep in something else.
This is the exordium of every future project.
c-programming-language-study
K&R C Programming Language Study Companion
Built complete study companion for The C Programming Language (Kernighan & Ritchie, 2nd edition). All 8 chapters with inline callouts, terminal-drop blocks, concept admonitions, pre-rendered diagrams, and exercise trackers.
What was built
-
8 chapter partials — ch01 through ch08, every section documented with inline code + callouts
-
34 C source files —
scripts/ch01/throughscripts/ch08/, all compilable with{compiler} {compiler-flags} -
5 pre-rendered diagrams — D2 and Graphviz in
images/ch01/(startup sequence, state machine, array layout, call-by-value stack, string memory) -
Education standard —
partials/education-std.adocbehindifdef::show-std[] -
9 custom admonition roles —
[TIP.terminal],[IMPORTANT.concept],[NOTE.strace],[NOTE.reference],[NOTE.compile],[WARNING.debug],[TIP.exercise],[NOTE.mission],[NOTE.progress] -
STD-025 created — universal AsciiDoc authoring standard with diagrams (D2 taxonomy, Mermaid flow, Graphviz architecture)
How to open
# Build
cd ~/atelier/_bibliotheca/domus-captures && make build-data PROJ=c-programming
# View
firefox data/d000/education/c-programming-language/output/c-programming-language.html &
# Build with education standard visible
make build-data PROJ=c-programming ENV=show-std
References
-
Standard:
data/d000/education/EDUCATION-ASCIIDOC-STD.adoc -
STD-025:
docs/modules/ROOT/pages/standards/STD-025-asciidoc-authoring.adoc -
CSS roles:
domus-asciidoc-build/docinfo/css-light-cyan/admonitions.css -
Project:
data/d000/education/c-programming-language/
domus-nvim-overhaul
domus-nvim — C Support & AsciiDoc Snippets
Continued overhaul from yesterday. Focus today: C/systems programming support, rainbow-delimiters crash fix, AsciiDoc snippet expansion.
Changes
-
C/systems programming —
ftplugin/c.luawith<leader>cr(compile+run),<leader>cm(quickfix),K(man pages for syscalls). clangd handler with--clang-tidy,--header-insertion=iwyu. DAP config for C withcwd/default. 15 C snippets (main, syscall, fork, mmap, socket, signal, pthread, etc.) -
rainbow-delimiters fix — disabled for
vimfiletype via condition function. Local strategy crashed on nil treesitter nodes in regex-heavy syntax files. -
AsciiDoc snippets — 40+ new snippets: titled block admonitions (all 5 types), choice-based
admon, complex admonitions, tabs (@asciidoctor/tabs), anchors, icons, tables (5 variants), lists (nested, steps). Block admonition keymaps:<leader>aBn/aBt/aBw/aBc/aBi(normal + visual wrap). -
Harpoon keymap — moved from
<leader>ato<leader>hato resolve collision with AsciiDoc prefix.
How to open
NVIM_APPNAME=domus-nvim nvim
# or alias: v
Commits
d624b5c, dc5e84a, 6cd602e, bf35771, 61d14aa
References
-
Repo:
~/atelier/_projects/personal/domus-nvim -
Antora docs:
make docs-serve(port 8000) -
Config dir:
~/.config/domus-nvim(symlink)
apparmor-mmdc-sandbox
AppArmor Blocking mmdc (Mermaid CLI) — Chromium Sandbox Denied
Symptom
Building C programming study companion with make build-data PROJ=c-programming — Mermaid diagram blocks fail:
asciidoctor: ERROR: partials/ch01-tutorial-intro.adoc: line 58: Failed to generate image: mmdc failed:
Direct mmdc test confirms:
echo 'sequenceDiagram
A->>B: hello' | mmdc -i - -o /tmp/test.png (1)
| 1 | Fails with No usable sandbox! — Puppeteer cannot launch headless Chromium. |
Error
Error: Failed to launch the browser process!
[FATAL:zygote_host_impl_linux.cc(128)] No usable sandbox!
If you are running on Ubuntu 23.10+ or another Linux distro that has disabled
unprivileged user namespaces with AppArmor, see
https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md
Root Cause
AppArmor restricts unprivileged_userns_clone. Chromium’s sandbox uses clone() with CLONE_NEWUSER to create a user namespace — AppArmor denies this.
This is the same mechanism that protects against container escape exploits. Chromium needs it for process isolation. AppArmor blocks it because unprivileged user namespaces are a known privilege escalation vector (CVE-2023-2640, CVE-2023-32629).
# Verify the restriction
cat /proc/sys/kernel/unprivileged_userns_clone (1)
sysctl kernel.unprivileged_userns_clone (2)
aa-status | grep chromium (3)
| 1 | 0 = disabled (AppArmor default on Arch with hardened kernel) |
| 2 | Same check via sysctl |
| 3 | Check if Chromium has an AppArmor profile loaded |
Pattern: Three AppArmor Incidents in 4 Days
| Date | What broke | Root cause | Fix |
|---|---|---|---|
2026-06-25 |
|
|
Narrowed deny scope — allow |
2026-06-27 |
|
gopass profile has no |
Pending — need to add |
2026-06-28 |
|
AppArmor blocks |
Replaced Mermaid with D2 in diagrams (D2 doesn’t need a browser) |
What this pattern teaches
|
AppArmor is a deny-by-default system
Every program that runs under an AppArmor profile can ONLY do what the profile explicitly allows. This includes:
The three incidents show three different facets of the same constraint:
Each one requires a different diagnostic approach:
|
Workaround Applied
Replaced Mermaid ([mermaid,format=png]) with D2 ([d2,format=png]) for all diagrams in the C programming study companion. D2 renders natively without a browser — no Puppeteer, no Chromium, no sandbox.
Diagnostic Commands for Future Reference
# Check if unprivileged user namespaces are enabled
sysctl kernel.unprivileged_userns_clone (1)
# Check AppArmor status for a specific binary
aa-status | grep -A5 'mmdc\|chromium\|puppeteer' (2)
# Watch for AppArmor denials in real time
sudo dmesg -w | grep -i 'DENIED\|apparmor' (3)
# Check audit log for recent denials
sudo ausearch -m AVC -ts recent | grep -i 'DENIED' (4)
# Trace the failing process
strace -f -e clone,clone3 mmdc -i input.mmd -o output.png 2>&1 | grep -i 'EPERM\|EACCES' (5)
| 1 | Returns 0 if disabled (default on hardened Arch). 1 if enabled. |
| 2 | Shows which AppArmor profiles are loaded for diagram tools. |
| 3 | Real-time kernel log — DENIED messages appear here for implicit denials. |
| 4 | Audit framework — more structured than dmesg. Requires auditd running. |
| 5 | Traces the clone() syscall that creates the user namespace. EPERM confirms AppArmor denial. |
Options NOT Taken (and why)
| Option | How | Why not |
|---|---|---|
Enable unprivileged userns |
|
Opens privilege escalation vector. Defeats the purpose of AppArmor hardening. |
Run mmdc with |
|
Disables Chromium’s process isolation. Renders untrusted Mermaid input in an unsandboxed browser. |
Create AppArmor profile for Chromium allowing userns |
|
Too broad — would apply to all Chromium instances, not just mmdc. |
Use Kroki server instead |
Offload rendering to |
Works for Antora builds (already configured). Not available for standalone |
TODO
-
Investigate Kroki as a local diagram server for standalone builds (Docker required)
-
Consider
d2as the primary diagram language — no browser dependency, cleaner syntax, native binary -
Document this pattern in the AppArmor codex page
parental-controls-p50
Parental Controls — gabriel-ThinkPad-P50
Remote administration of son’s workstation via SSH. DNS-level blocking via /etc/hosts + chattr +i immutable flag. Network visibility via lsof. Firefox forensics via places.sqlite (browsing history) and recovery.jsonlz4 (active tabs).
Session — 2026-06-28
Remote administration of gabriel-ThinkPad-P50 (10.50.10.107) via SSH from modestus-p16g.
SSH Config Fix
echo -e '\nHost modestus-p50\n HostName 10.50.10.170' >> ~/.ssh/config.d/80-d000-hosts.conf (1)
sed -i 's/10\.50\.10\.170/10.50.10.107/' ~/.ssh/config.d/80-d000-hosts.conf (2)
grep '10.50.10.107' ~/.ssh/config.d/80-d000-hosts.conf (3)
ssh modestus-p50 (4)
| 1 | Initial entry — wrong last octet (170 instead of 107) |
| 2 | sed -i in-place fix — verify-before/change/verify-after from memory |
| 3 | Verify-after — confirms the substitution took |
| 4 | SSH in — host entry resolves, password auth to the P50 |
Hosts File — DNS-Level Blocking
cat /etc/hosts (1)
sudo chattr +i /etc/hosts (2)
lsattr /etc/hosts (3)
# ----i---------e------- /etc/hosts
| 1 | Verify blocks are active — 127.0.0.1 entries for YouTube, TikTok, Twitch, Reddit, streaming services. Lincoln Learning domains left unblocked (educational — allowed). |
| 2 | chattr +i — set immutable attribute. Even root cannot modify, delete, or rename the file. Requires chattr -i to unlock. Prevents the child from editing /etc/hosts even with sudo access. |
| 3 | lsattr — verify the i (immutable) flag is set. The e flag is extent-based (filesystem default — ignore it). |
Network Visibility — lsof
sudo lsof -i -n -P | grep -i 'firefox\|chrome\|ESTABLISHED' | awk '{print $1, $3, $9}' | sort -u (1)
| 1 | -i network connections only. -n no DNS resolution (fast). -P no port name resolution (show numbers). Piped through awk to extract process name ($1), user ($3), and connection ($9). sort -u deduplicates. |
firefox gabriel 10.50.10.107:33490->142.251.152.119:443 # Google/YouTube
firefox gabriel 10.50.10.107:58624->151.101.65.42:443 # Reddit (151.101.x.x = Fastly/Reddit CDN)
firefox gabriel 10.50.10.107:39956->34.107.243.93:443 # Google services
steam gabriel 10.50.10.107:39317->162.254.195.75:27018 # Steam (allowed)
gameoverl gabriel 127.0.0.1:53638->127.0.0.1:57343 # Ravenfield game (local, allowed)
Selective Process Kill
sudo pkill -u gabriel firefox (1)
| 1 | -u gabriel kills only processes owned by that user. Firefox dies, Steam and the game stay running. DNS cache dies with the browser — reopening Firefox picks up the hosts file blocks. |
Firefox Forensics — Browser History & Session Database
Firefox stores browsing history in SQLite databases under the user’s profile:
sudo find /home/gabriel/.mozilla/firefox -name 'places.sqlite' (1)
| 1 | places.sqlite contains all browsing history, bookmarks, and visit counts. One file per Firefox profile. |
PROFILE=$(sudo find /home/gabriel/.mozilla/firefox -name 'places.sqlite' -print -quit)
sudo sqlite3 "$PROFILE" "SELECT datetime(last_visit_date/1000000,'unixepoch','localtime') AS visited, url FROM moz_places WHERE last_visit_date IS NOT NULL ORDER BY last_visit_date DESC LIMIT 50;" (1)
| 1 | last_visit_date is microseconds since epoch — divide by 1,000,000 for Unix timestamp. datetime() with 'localtime' converts to human-readable in local timezone. |
sudo sqlite3 "$PROFILE" "SELECT datetime(last_visit_date/1000000,'unixepoch','localtime'), url FROM moz_places WHERE url LIKE '%youtube%' OR url LIKE '%reddit%' ORDER BY last_visit_date DESC LIMIT 20;" (1)
| 1 | Case-insensitive LIKE search — catches youtube.com, m.youtube.com, youtu.be, etc. This shows exactly what was accessed and when, even if the browser cache is cleared. |
sudo sqlite3 "$PROFILE" "SELECT SUBSTR(url, INSTR(url,'://')+3, INSTR(SUBSTR(url,INSTR(url,'://')+3),'/')-1) AS domain, SUM(visit_count) AS visits FROM moz_places WHERE visit_count > 0 GROUP BY domain ORDER BY visits DESC LIMIT 20;" (1)
| 1 | Extracts the domain from the full URL using SUBSTR + INSTR (no regex in SQLite). Groups by domain and sums visit counts. Shows which sites get the most traffic — the real usage pattern. |
sudo python3 <<'EOF'
import json, lz4.block, sys, glob
profiles = glob.glob('/home/gabriel/.mozilla/firefox/*/sessionstore-backups/recovery.jsonlz4')
if not profiles:
print("No active session found")
sys.exit(0)
with open(profiles[0], 'rb') as f:
magic = f.read(8)
data = json.loads(lz4.block.decompress(f.read()))
for w in data.get('windows', []):
for t in w.get('tabs', []):
entries = t.get('entries', [])
if entries:
print(entries[-1].get('url', ''))
EOF
(1)
| 1 | Firefox stores the active session as LZ4-compressed JSON. The recovery.jsonlz4 file contains every open tab across all windows. Requires python3 + python-lz4 — if lz4 isn’t installed: sudo apt install python3-lz4 (Ubuntu/Debian on the P50). |
Monitoring Commands — Quick Reference
| Task | Command |
|---|---|
What’s he browsing now? |
|
Kill browser only |
|
History (last 50) |
|
YouTube attempts |
|
Open tabs right now |
LZ4 Python script above |
Lock hosts file |
|
Unlock hosts file |
|
Verify lock |
|
firefox-forensics-findings
Describe this sub-partial here.
Both options worked. Gabriel was watching Napoleon Dynamite clips at midnight — good to know. Now let me build this properly as a partial that covers all three learning domains: SQLite/database syntax, Linux process management, and browser security.
Browser Forensics, Process Management, and SQLite Queries
Finding the Firefox Profile
Firefox stores all history, bookmarks, and session data in a SQLite database
called places.sqlite. Its location depends on how Firefox was installed.
find /home/$USER/.mozilla/firefox -name 'places.sqlite' 2>/dev/null (1)
find /home/$USER/snap/firefox -name 'places.sqlite' 2>/dev/null (2)
| 1 | Standard Firefox install path — used when installed via apt, pacman,
or similar package managers. $USER expands to the current user’s name.
2>/dev/null suppresses permission errors so only real hits are shown. |
| 2 | Snap-sandboxed Firefox path — Ubuntu and derivatives install Firefox as
a snap by default. The profile lives under ~/snap/firefox/common/ instead
of ~/.mozilla/. Always check both when the first returns nothing. |
Querying Another User’s Profile
When Firefox belongs to a different user (gabriel), two things change:
you need sudo to read their files, and you need to know their snap path.
PROFILE=$(sudo find /home/gabriel/snap/firefox \ (1)
-name 'places.sqlite' -print -quit) (2)
echo "$PROFILE" (3)
| 1 | sudo find — required because /home/gabriel/ is owned by gabriel.
Without sudo you get Permission denied and an empty result. |
| 2 | -print -quit — print the first match and stop immediately. Without
-quit, find continues searching and may return multiple results if
there are profile backups. We only want the live database. |
| 3 | Always echo "$PROFILE" before using it in a query. Verify the variable
holds the path you expect before passing it to sqlite3. |
The Database Lock Problem
Firefox holds an exclusive write lock on places.sqlite while it is running.
Any direct query attempt returns:
Error: in prepare, database is locked (5)
Two safe approaches:
Option A — Copy and Query (Recommended)
sudo cp "$PROFILE" /tmp/places-copy.sqlite (1)
sudo sqlite3 /tmp/places-copy.sqlite " (2)
SELECT datetime(last_visit_date/1000000,'unixepoch','localtime'), url
FROM moz_places
WHERE url LIKE '%youtube%'
OR url LIKE '%reddit%'
ORDER BY last_visit_date DESC
LIMIT 20;"
| 1 | Copy the database to /tmp while Firefox holds the lock. The copy
succeeds because the filesystem allows reads even on a locked file —
only SQLite’s own locking protocol prevents concurrent writers. The
copy is a snapshot of the database at that moment. |
| 2 | Query the copy. No lock contention. Safe to run while Firefox is open. The data may be slightly behind what Firefox has buffered in its WAL (write-ahead log) but is accurate enough for history inspection. |
Option B — Immutable URI (Query Live File Read-Only)
sudo sqlite3 \
"file://$PROFILE?mode=ro&immutable=1" \ (1)
"SELECT ..."
| 1 | The file:// URI scheme with ?mode=ro&immutable=1 tells SQLite to
open the file read-only and skip all locking checks entirely. immutable=1
is the critical flag — it tells SQLite "I promise nothing will change
this file while I’m reading it." Use this only when you know Firefox
will not write during your query. Option A is safer for production use. |
The SQLite Query — Anatomy
SELECT
datetime(last_visit_date/1000000, 'unixepoch', 'localtime'), (1)
url (2)
FROM moz_places (3)
WHERE url LIKE '%youtube%' (4)
OR url LIKE '%reddit%'
ORDER BY last_visit_date DESC (5)
LIMIT 20; (6)
| 1 | last_visit_date is stored in microseconds since Unix epoch (January 1,
1970 UTC). Dividing by 1000000 converts to seconds. 'unixepoch' tells
SQLite the input is a Unix timestamp. 'localtime' converts from UTC to
the system’s local timezone. Without 'localtime' all times show as UTC. |
| 2 | url is the full URL string exactly as Firefox recorded it. |
| 3 | moz_places is Firefox’s main URL table. Every URL ever visited gets a
row here. Related tables: moz_historyvisits (individual visit events),
moz_bookmarks (bookmarks), moz_inputhistory (address bar searches). |
| 4 | LIKE '%youtube%' matches any URL containing the string youtube.
The % is a wildcard meaning "zero or more of any character." This
catches youtube.com, www.youtube.com, and any subdomain or path. |
| 5 | ORDER BY last_visit_date DESC — newest visits first. DESC means
descending (highest number = most recent timestamp = shown first). |
| 6 | LIMIT 20 — return at most 20 rows. Always use a LIMIT when exploring
an unfamiliar database. moz_places can contain hundreds of thousands
of rows. |
Explore the Full Database Schema
sudo sqlite3 /tmp/places-copy.sqlite ".tables" (1)
sudo sqlite3 /tmp/places-copy.sqlite ".schema moz_places" (2)
sudo sqlite3 /tmp/places-copy.sqlite "
SELECT name, sql FROM sqlite_master
WHERE type='table'
ORDER BY name;" (3)
| 1 | .tables is a SQLite dot-command (not SQL). Lists all table names in
the database. Dot-commands start with . and are SQLite shell commands,
not part of the SQL language. |
| 2 | .schema moz_places shows the CREATE TABLE statement for that table —
every column name and its data type. This is how you discover what you
can query without reading Firefox source code. |
| 3 | sqlite_master is SQLite’s internal catalog table. Every database has
one. It stores the schema for all tables, indexes, triggers, and views.
Querying it directly gives you the same information as .schema but
in a format you can filter and sort with SQL. |
Process Inspection and Session Management
See What Is Running as Another User
ps aux | grep -iE 'firefox|chrome|chromium' | grep -v grep (1)
ps -u gabriel --forest (2)
| 1 | ps aux lists every process on the system. a = all users, u =
user-oriented format (shows username, CPU, memory), x = include
processes not attached to a terminal. grep -v grep removes the grep
process itself from the results. |
| 2 | --forest shows processes as a tree — you can see which processes are
children of which parent. Firefox spawns many content processes; --forest
shows them all branching from the main firefox PID. |
Find the Main Firefox PID
pgrep -u gabriel firefox (1)
pgrep -u gabriel -a firefox (2)
cat /proc/$(pgrep -u gabriel firefox | head -1)/status (3)
| 1 | pgrep searches for processes by name and returns PIDs only. -u gabriel
filters to gabriel’s processes. Cleaner than ps aux | grep. |
| 2 | -a adds the full command line to the output so you can confirm which
PID is the main process vs content processes. |
| 3 | /proc/PID/status is a virtual file in the Linux proc filesystem. It
shows the process’s real state: name, PID, parent PID, memory usage,
and which user owns it. This is where ps gets its data. |
Close Firefox Gracefully for a Specific User
sudo -u gabriel DISPLAY=:0 wmctrl -c firefox (1)
sudo kill -SIGTERM $(pgrep -u gabriel firefox | head -1) (2)
sudo kill -SIGKILL $(pgrep -u gabriel firefox | head -1) (3)
| 1 | Sends a close-window request through the X11 window manager. Polite —
Firefox gets a chance to save session state and flush the database.
Requires wmctrl (apt install wmctrl). Use this first. |
| 2 | SIGTERM (signal 15) asks the process to terminate cleanly. Firefox
handles this signal and shuts down gracefully, flushing all pending
writes to places.sqlite before exiting. |
| 3 | SIGKILL (signal 9) is unconditional — the kernel kills the process
immediately with no cleanup. Use only if SIGTERM does not work within
a few seconds. Risk: the database WAL may not be flushed, leaving
places.sqlite in an inconsistent state. |
After Closing — Query the Live Database Directly
PROFILE=$(sudo find /home/gabriel/snap/firefox \
-name 'places.sqlite' -print -quit)
sudo sqlite3 "$PROFILE" "
SELECT datetime(last_visit_date/1000000,'unixepoch','localtime'), url
FROM moz_places
ORDER BY last_visit_date DESC
LIMIT 30;"
No copy needed once Firefox is closed — the lock is released.
Browser Security Concepts
What places.sqlite Reveals
| Table | Contains |
|---|---|
|
Every URL ever visited — full URL, visit count, last visit timestamp |
|
Individual visit events — one row per visit, with referrer and transition type |
|
All bookmarks and folder structure |
|
What the user typed into the address bar (not always the full URL) |
|
Active session cookies — in |
What This Means for Security
A standard user account on a shared Linux machine with sudo access can:
-
Read any other user’s complete browser history
-
Extract active session cookies from
cookies.sqlite -
Read saved form data and address bar history
This is expected behavior — sudo is designed to grant this level of access.
The security boundary on a shared machine is sudo access itself, not the
browser’s storage.
For a family machine where you want gabriel to have privacy from other
standard users (but not from root/sudo), the filesystem permissions are
already correct — /home/gabriel/ is 700 (owner only). Only sudo
bypasses this.
Check Cookie Database (Session Tokens)
COOKIES=/home/gabriel/snap/firefox/common/.mozilla/firefox/9srvkhla.default/cookies.sqlite
sudo cp "$COOKIES" /tmp/cookies-copy.sqlite
sudo sqlite3 /tmp/cookies-copy.sqlite "
SELECT host, name, datetime(expiry,'unixepoch','localtime') as expires
FROM moz_cookies
ORDER BY lastAccessed DESC
LIMIT 20;"
This shows active session cookies by most recently accessed. Cookie names
like session_token, auth, sid, or JSESSIONID are session identifiers
— possessing them is equivalent to being logged in as that user on that site.
Clean Up Temp Files
sudo rm /tmp/places-copy.sqlite /tmp/cookies-copy.sqlite
Always remove database copies from /tmp after use. /tmp is world-readable
on most Linux systems.
Navigation Chain — moz_historyvisits
sudo sqlite3 /tmp/places-copy.sqlite ".schema moz_historyvisits"
CREATE TABLE moz_historyvisits (
id INTEGER PRIMARY KEY,
from_visit INTEGER, (1)
place_id INTEGER, (2)
visit_date INTEGER, (3)
visit_type INTEGER, (4)
session INTEGER,
source INTEGER DEFAULT 0 NOT NULL,
triggeringPlaceId INTEGER
);
| 1 | from_visit — the visit ID that led to this one. Join back to this same table to reconstruct the click chain: page A → page B → page C. |
| 2 | place_id — foreign key to moz_places.id. Join to get the actual URL. |
| 3 | visit_date — microseconds since epoch, same as moz_places.last_visit_date. |
| 4 | visit_type — 1=link click, 2=typed in address bar, 3=bookmark, 4=embed, 5=redirect (permanent), 6=redirect (temporary), 7=download. |
sudo sqlite3 /tmp/places-copy.sqlite "
SELECT datetime(v.visit_date/1000000,'unixepoch','localtime') AS visited,
p.url AS destination,
p2.url AS came_from
FROM moz_historyvisits v
JOIN moz_places p ON v.place_id = p.id
LEFT JOIN moz_historyvisits v2 ON v.from_visit = v2.id
LEFT JOIN moz_places p2 ON v2.place_id = p2.id
ORDER BY v.visit_date DESC
LIMIT 20;" (1)
| 1 | Three-table JOIN: visit → destination URL + referrer visit → referrer URL. Shows exactly how he navigated — which link on which page led to where. LEFT JOIN handles direct navigation (typed URL, bookmark) where from_visit is 0. |
Usage Analysis — gabriel-ThinkPad-P50 (2026-06-28)
Domain Visit Frequency
sudo sqlite3 -header -column /tmp/places-copy.sqlite " (1)
SELECT SUBSTR(url, INSTR(url,'://')+3,
INSTR(SUBSTR(url,INSTR(url,'://')+3),'/')-1) AS domain,
COUNT(*) AS visits,
printf('%.1f%%', COUNT(*) * 100.0 / (SELECT COUNT(*) FROM moz_places WHERE visit_count > 0)) AS pct (2)
FROM moz_places
WHERE visit_count > 0
GROUP BY domain
ORDER BY visits DESC
LIMIT 20;"
| 1 | -header -column — formatted table output with column headers and aligned spacing. Without these flags, SQLite defaults to | pipe-delimited which is functional but hard to read at a glance. |
| 2 | printf('%.1f%%') — percentage of total visits. The subquery (SELECT COUNT(*) …) calculates the denominator inline. Double %% escapes the literal percent sign in SQLite’s printf. |
| Domain | Visits | Assessment |
|---|---|---|
|
5,806 |
BLOCKED — 61% of all browsing. Hosts file now redirects to 127.0.0.1 |
|
1,190 |
Allowed — school LMS (Lincoln Learning) |
|
228 |
Allowed — search |
|
193 |
Allowed — Lincoln Learning API backend |
|
149 |
BLOCKED — redirected to 127.0.0.1 |
|
62 |
Allowed — Lincoln Learning (Agilix Buzz LMS engine) |
|
56 |
Allowed — Google auth |
|
51 |
Monitor — Steam store browsing (game purchases) |
|
48 |
Allowed — math/education platform |
|
20 |
Allowed — education |
|
20 |
BLOCKED — YouTube Music, redirected to 127.0.0.1 |
|
17 |
Monitor — AI usage for schoolwork (verify not used for answers) |
|
16 |
Local — likely a dev server or game server |
Validated Output — 2026-06-28
domain visits pct
--------------------------------------------------------- ------ -----
www.youtube.com 5806 69.6%
lincolnlearningsolutions.lms.lincolnlearningsolutions.org 1190 14.3%
www.google.com 228 2.7%
pulse-api.llsapps.org 193 2.3%
www.youtubekids.com 149 1.8%
lls-prod-oh-pulse.auth.us-east-2.amazoncognito.com 111 1.3%
api.agilixbuzz.com 62 0.7%
accounts.google.com 56 0.7%
store.steampowered.com 51 0.6%
www.ixl.com 48 0.6%
global-zone20.renaissance-go.com 39 0.5%
docs.google.com 37 0.4%
clever.com 30 0.4%
www.khanacademy.org 20 0.2%
music.youtube.com 20 0.2%
login.renaissance.com 19 0.2%
zone20-student.renaissance-go.com 18 0.2%
chatgpt.com 17 0.2%
127.0.0.1:8080 16 0.2%
cyber.gale.com 13 0.2%
YouTube (all variants: www + kids + music) = 71.6% of all browsing. Educational sites (Lincoln Learning + IXL + Khan Academy + Renaissance) = 16.3%. The blocking is justified.
Port 8080 Investigation
127.0.0.1:8080 appeared 16 times in browsing history. Investigated via lsof:
sudo lsof -i :8080 -u gabriel | head -10 (1)
| 1 | -i :8080 filters to port 8080 only. -u gabriel scopes to his user. The output showed systemd process entries but no active listener — the service that ran on 8080 is no longer running. Likely a local game server or dev tool that was previously active. |
Hourly Browsing Pattern
sudo sqlite3 -header -column /tmp/places-copy.sqlite "
SELECT strftime('%H', datetime(visit_date/1000000,'unixepoch','localtime')) AS hour,
COUNT(*) AS visits,
printf('%.*s', COUNT(*)/20, '████████████████████████████████████████████████████') AS bar (1)
FROM moz_historyvisits
GROUP BY hour
ORDER BY hour;"
| 1 | Visual bar chart — printf('%.*s', n, string) prints n characters of the bar string. Dividing visit count by 20 scales the bar to fit the terminal. Same technique works against ISE DataConnect for auth timeline visualization. |
Hour Visits Pattern
──── ────── ───────────────────────────────────────
00 131 ██████ ← after midnight
01 38 █
06 17
07 91 ████
08 231 ███████████
09 694 ██████████████████████████████████
10 624 ███████████████████████████████
11 618 ██████████████████████████████
12 1,149 ████████████████████████████████████████████████████████ ← peak: lunch
13 731 ████████████████████████████████████
14 600 ██████████████████████████████
15 617 ██████████████████████████████
16 1,017 ██████████████████████████████████████████████████ ← after school
17 1,101 ██████████████████████████████████████████████████████
18 1,072 █████████████████████████████████████████████████████
19 789 ███████████████████████████████████████
20 731 ████████████████████████████████████
21 1,031 ███████████████████████████████████████████████████ ← evening peak
22 628 ███████████████████████████████
23 357 █████████████████
Key findings:
-
No browsing 02:00–05:00 — good, he’s sleeping
-
Peak at 12:00 (1,149) — lunch break, unsupervised
-
Heavy 16:00–21:00 (1,017–1,101 per hour) — after school through evening
-
131 visits at midnight — needs a bedtime network cutoff
Late-Night Usage — Evidence
sudo sqlite3 -header -column /tmp/places-copy.sqlite "
SELECT datetime(visit_date/1000000,'unixepoch','localtime') AS visited,
p.url
FROM moz_historyvisits v
JOIN moz_places p ON v.place_id = p.id
WHERE strftime('%H', datetime(visit_date/1000000,'unixepoch','localtime')) >= '22'
OR strftime('%H', datetime(visit_date/1000000,'unixepoch','localtime')) < '06'
ORDER BY visit_date DESC
LIMIT 30;" (1)
| 1 | strftime('%H') extracts the hour as a zero-padded string. The WHERE clause selects 22:00–05:59 — everything outside reasonable hours for a child. JOIN resolves the place_id to the actual URL. |
visited url
------------------- --------------------------------------------------------------------------
2026-06-28 01:09:57 https://www.youtube.com/watch?v=kQVxj0WGD4g
2026-06-28 00:59:17 https://www.youtube.com/watch?v=8Vom5GZdK_I
2026-06-28 00:59:12 https://www.youtube.com/watch?v=NnTycJg1MIo
2026-06-28 00:52:12 https://www.youtube.com/watch?v=da7AZFp7f8A
2026-06-28 00:46:42 https://www.youtube.com/watch?v=gducgTcJMMU
2026-06-28 00:44:49 https://www.youtube.com/watch?v=lsKRlVwXThU
2026-06-28 00:43:25 https://www.youtube.com/watch?v=L3LHAlcrTRA
2026-06-28 00:41:50 https://www.youtube.com/watch?v=bya709eOwg0
2026-06-28 00:40:51 https://www.youtube.com/watch?v=ROFeU9_tFf4
2026-06-28 00:40:42 https://www.youtube.com/results?search_query=napoleon+dynamite
2026-06-28 00:38:44 https://www.youtube.com/watch?v=BiAwpYIkRmU
2026-06-28 00:38:40 https://www.youtube.com/results?search_query=napoleon+dynamite+dance
2026-06-28 00:37:51 https://www.youtube.com/watch?v=mO1oBfG59Xw
2026-06-28 00:37:32 https://www.youtube.com/watch?v=OzTX8SVJtis
2026-06-28 00:37:16 https://www.youtube.com/watch?v=mEQvIdPnrUw
2026-06-28 00:36:42 https://www.youtube.com/watch?v=NZJrGuC92U8
2026-06-28 00:35:09 https://www.youtube.com/watch?v=pttdENG0X78
2026-06-28 00:35:03 https://www.youtube.com/results?search_query=kip+durag
2026-06-28 00:34:50 https://www.youtube.com/
2026-06-28 00:33:06 https://www.youtube.com/watch?v=jWYHk3E2qpE
2026-06-28 00:32:50 https://www.youtube.com/watch?v=mdcbiy0Ml7U
2026-06-28 00:32:44 https://www.youtube.com/results?search_query=ralph+sopranos+shopping+cart
2026-06-28 00:29:26 https://www.youtube.com/watch?v=3hLfGlz97Ic
2026-06-28 00:29:19 https://www.youtube.com/results?search_query=prison+break++how+goes+it+roy
2026-06-28 00:28:55 https://www.youtube.com/results?search_query=prison+how+goes+it+roy
2026-06-28 00:28:20 https://www.youtube.com/
2026-06-26 23:53:25 https://www.youtube.com/shorts/WgYhWWhM8MI
2026-06-26 23:44:08 https://www.youtube.com/shorts/WgYhWWhM8MI
Summary: 28 of 30 late-night entries are YouTube. Continuous session from 00:28 to 01:09 on 2026-06-28 — 41 minutes of YouTube after midnight. Search queries include Napoleon Dynamite clips, Sopranos scenes, and Prison Break. Previous night (2026-06-26) active at 23:44–23:53 watching YouTube Shorts. This pattern justifies a bedtime network cutoff.
Recommended Actions
| Action | Method | Status |
|---|---|---|
Block YouTube/TikTok/Reddit/Twitch |
|
✓ Done — 2026-06-28 |
Monitor Steam store usage |
Periodic |
Manual — consider blocking if grades drop |
ChatGPT usage review |
Query |
Pending — check if used for homework answers |
Bedtime network cutoff |
Cron job: |
Planned — implement via VyOS firewall rule or P50 local |
Weekly usage report |
Script: domain + hourly queries → formatted output → email or file |
Planned — automate as |
yubikey-age-gopass-v4
YubiKey Age Identity + Gopass v4 Store
Hardware-bound secrets upgrade. Software age key on disk → YubiKey-backed identity where decryption requires physical touch. New gopass v4 store with age backend replaces GPG — eliminates keyring management, agent crashes, and subkey expiration as failure modes. Coexistence with v3 during natural credential rotation.
Prerequisites — Verified 2026-06-28
| Component | State | Version |
|---|---|---|
YubiKey |
Detected — INC-004 USBGuard block resolved |
5C NFC, FW 5.7.1, serial 31311804 |
PIV |
Enabled (USB + NFC) |
— |
|
Working after |
— |
|
Installed from official repos ( |
0.5.1-1 |
|
age backend supported |
1.16.1 |
|
Socket-activated, CCID functional |
— |
Phase 1 — Generate YubiKey-Backed Age Identity
age-plugin-yubikey (1)
| 1 | Interactive setup — prompts to select a PIV slot (default: slot 82, Retired Key Management 1) and touch policy. Generates an age identity on the YubiKey and outputs the recipient (public key) and identity file path. The private key never leaves the YubiKey hardware. |
Expected output:
Choose a slot (1-20, or press Enter for slot 1):
Using slot 82 (Retired Key Management 1)
Generated new key pair.
recipient: age1yubikey1q...
identity: age-plugin-yubikey-1...
Write the identity to a file? (y/n)
Save the identity file and recipient:
mkdir -p ~/.age/identities ~/.age/recipients
# The identity file goes to ~/.age/identities/ (points to the YubiKey — no secret material)
# The recipient (public key) goes to ~/.age/recipients/yubikey.txt
Phase 2 — Test Encrypt/Decrypt Cycle
echo "YubiKey age test — $(date)" | age -r "$(cat ~/.age/recipients/yubikey.txt)" > /tmp/yubikey-age-test.age (1)
age -d -i ~/.age/identities/age-identity-yubikey.txt /tmp/yubikey-age-test.age (2)
rm /tmp/yubikey-age-test.age
| 1 | Encrypt with the YubiKey recipient (public key) — no hardware touch needed for encryption |
| 2 | Decrypt requires the YubiKey to be inserted AND touched (depending on touch policy configured in Phase 1). If the YubiKey blinks, touch it. |
Phase 3 — Initialize Gopass v4 Store
gopass init --store v4 --crypto age (1)
gopass config --store v4 (2)
| 1 | Creates a new gopass store named v4 with age backend (not GPG). Gopass will prompt for the age identity — provide the YubiKey identity file path. |
| 2 | Verify the store configuration — should show crypto: age and the correct identity. |
Test with a throwaway entry:
gopass insert v4/test/hello (1)
gopass show v4/test/hello (2)
gopass rm v4/test/hello (3)
| 1 | Insert a test secret — encrypted with age using the YubiKey recipient |
| 2 | Decrypt — requires YubiKey present + touch |
| 3 | Clean up test entry |
Phase 4 — Coexistence Model
Both stores run in parallel:
v3/ → GPG-backed (current production, read-only going forward)
v4/ → age/YubiKey-backed (new entries, rotated credentials)
-
New credentials →
v4/domains/d000/… -
Rotated credentials → new value in
v4/, old stays inv3/for rollback -
dsourceprofiles → update one at a time fromv3/tov4/paths -
Natural migration via credential rotation cycle — no batch migration script needed
PIV PIN Recovery — Near-Lockout Incident (2026-06-28)
Timeline
| Attempt | Action | Result |
|---|---|---|
1 |
|
|
2 |
|
|
3 |
STOPPED — 1 try remaining, switched to PUK recovery |
Avoided permanent lockout |
Root Cause
The PIV PIN had been changed from the factory default at some point in the past. ykman piv info reported WARNING: Using default PIN! — this warning checks the default value, not whether it’s actually set to that value. The warning is misleading.
Recovery
ykman piv access unblock-pin (1)
# Enter PUK: <default 12345678> (2)
# Enter a new PIN: <new pin>
# New PIN set.
| 1 | unblock-pin bypasses the PIN entirely — uses the PUK (PIN Unblocking Key) to set a new PIN regardless of the current PIN state. This is the only recovery path when PIN tries are exhausted. |
| 2 | The PUK was still at factory default (12345678, 3/3 tries remaining). If the PUK had also been exhausted, the PIV applet would be permanently locked — no recovery without a full YubiKey reset (destroys all PIV keys and certificates). |
Post-Recovery — Secure All Defaults
ykman piv access change-puk (1)
ykman piv access change-management-key --generate (2)
ykman piv info (3)
| 1 | Change PUK from factory default 12345678 — if someone knows the default PUK, they can reset your PIN and access your PIV keys. Change immediately. |
| 2 | --generate creates a random AES192 management key and stores it on the YubiKey (protected by PIN). The default management key is published in every YubiKey guide — leaving it unchanged is equivalent to no management key. |
| 3 | Verify: all three warnings (default PIN, default PUK, default Management key) should be gone. |
Store Credentials
gopass edit v3/domains/d000/security/yubikey/31311804 (1)
| 1 | Store the new PIN, PUK, and management key in gopass. The management key is long (24 bytes hex) — do not try to memorize it. If you lose the PIN, only the PUK can recover. If you lose both, the PIV applet is bricked. |
type: "yubikey"
serial: "31311804"
model: "YubiKey 5C NFC"
firmware: "5.7.1"
credentials:
piv:
pin: "<new-pin>"
puk: "<new-puk>"
management_key: "<generated-key>"
_use: "PIV operations — age-plugin-yubikey, SSH certs, code signing"
fido2:
pin: "<if set>"
_use: "WebAuthn, passkeys, SSH resident keys"
created: "2026-06-28"
updated: "2026-06-28"
Lessons
-
Never guess a PIV PIN — check tries remaining first (
ykman piv info) -
WARNING: Using default PIN!is unreliable — it checks against the known default, not actual state -
PUK is the last resort — if both PIN and PUK are exhausted, PIV is permanently locked
-
Change all defaults immediately on a new YubiKey — PIN, PUK, and management key
-
Store in gopass before proceeding — the management key is unrecoverable if lost
v4 Store — Operational (2026-06-28)
Incident Chain to Operational
| Blocker | Root Cause | Fix |
|---|---|---|
|
USBGuard blocking CCID interface |
|
|
PIV PIN changed from default at unknown point, 1 try remaining |
|
|
AES192 management key unsupported by age-plugin-yubikey |
|
|
AppArmor profile on |
Added |
|
gopass internal identity keyring at |
|
Finding gopass identity path |
Three guesses failed — |
|
Proof — strace Reveals the Path
strace -f -e openat gopass show v4/test/hello 2>&1 | grep -i 'age\|identity' (1)
| 1 | The syscall trace showed openat("~/.config/gopass/age/identities") = -1 ENOENT — gopass looks for its internal encrypted identity keyring at this exact path. Not ~/.age/, not age-identities.txt, not environment variables. The only way to populate it correctly is gopass age identities add. |
Correct Setup Procedure (for future reference)
gopass age identities add (1)
# Enter the age identity starting in AGE-:
# AGE-PLUGIN-YUBIKEY-1HNRA6QVZ8TWEDFSV4AGYG
# Provide the corresponding age recipient []:
# age1yubikey1qwg5w7vykg36czquwdseajl3rx07t0sdk4y6m96wfcep7k33gqpnu3x47yf
gopass show v4/test/hello (2)
# Enter secret: <PIN>
# 👆 Touch YubiKey
# test123
| 1 | gopass age identities add encrypts the identity token into its own keyring format at ~/.config/gopass/age/identities. This is the ONLY supported way to add age identities — manual file creation does not work. |
| 2 | Decrypt requires PIN entry + physical YubiKey touch. The private key never leaves the hardware. |
v3 → v4 Migration
Copy v3 entries to v4 — v3 stays untouched as read-only fallback:
gopass ls --flat v3/ | head -20 (1)
gopass show v3/domains/d000/identity/ise/ise-01 | gopass insert v4/domains/d000/identity/ise/ise-01 (2)
gopass show v4/domains/d000/identity/ise/ise-01 (3)
| 1 | List v3 entries — see what needs migrating. No rush — migrate as you rotate credentials. |
| 2 | Pipe v3 decrypt (GPG) → v4 encrypt (age/YubiKey). The entry contents are identical — only the encryption backend changes. v3 is unmodified. |
| 3 | Verify — PIN + touch to decrypt from v4. Compare output with v3 to confirm integrity. |
Batch copy (all v3 entries at once — optional, only when ready):
gopass ls --flat v3/ | while read -r entry; do
printf 'Migrating: %s\n' "$entry"
gopass show "$entry" | gopass insert "v4/${entry#v3/}" (1)
done
| 1 | ${entry#v3/} strips the v3/ prefix — v3/domains/d000/… becomes v4/domains/d000/…. Each entry is decrypted from v3 (GPG) and re-encrypted into v4 (age/YubiKey). Touch required for each entry (PIN policy: Always). |
Coexistence:
v3/ → GPG-backed (read-only, rollback)
v4/ → age/YubiKey (primary, new entries)
-
New credentials →
v4/ -
Rotated credentials → new value in
v4/, old stays inv3/ -
dsourceprofiles → update fromv3/tov4/one at a time -
Retire v3 when all entries are in v4 and tested
Dual-Passphrase Architecture
The Two Layers
Layer 1: gopass keyring passphrase ← something you KNOW (protects the identity token)
Layer 2: YubiKey PIN + physical touch ← something you HAVE + DO (protects the private key)
| Layer | What It Protects | Where It Lives |
|---|---|---|
Keyring passphrase |
The |
Your memory — not stored on disk in plaintext |
YubiKey PIN |
The P-256 private key burned into the YubiKey PIV slot |
YubiKey hardware — entered per-decrypt (PIN policy: Always) |
Physical touch |
Authorization of each decrypt operation |
Your finger on the YubiKey — cannot be automated or replayed |
Why They Must Be Different
If the keyring passphrase equals the YubiKey PIN, compromising one compromises both layers. The security model collapses to single-factor:
-
Same password: attacker with PIN → opens keyring → has identity token → decrypts (only needs YubiKey physical access)
-
Different passwords: attacker with PIN → blocked at keyring. Attacker with keyring passphrase → blocked at PIN. Both required.
Comparison: v3 (GPG) vs v4 (age/YubiKey)
| v3 (GPG) | v4 (age/YubiKey) | |
|---|---|---|
Factor 1 |
GPG passphrase |
Gopass keyring passphrase |
Factor 2 |
— |
YubiKey PIN |
Factor 3 |
— |
Physical touch |
Key location |
|
YubiKey silicon (hardware — non-extractable) |
Agent caching |
|
No caching (PIN policy: Always) |
Compromise path |
Disk access → key extraction → offline brute-force |
Physical YubiKey theft + PIN + keyring passphrase (three factors) |
Changing the Keyring Passphrase — Executed 2026-06-28
gopass age identities (1)
# age1yubikey1qwg5w7vykg36czquwdseajl3rx07t0sdk4y6m96wfcep7k33gqpnu3x47yf
gopass age identities rm age1yubikey1qwg5w7vykg36czquwdseajl3rx07t0sdk4y6m96wfcep7k33gqpnu3x47yf (2)
gopass age identities add (3)
# Enter the age identity starting in AGE-:
# AGE-PLUGIN-YUBIKEY-1HNRA6QVZ8TWEDFSV4AGYG
# Provide the corresponding age recipient []:
# age1yubikey1qwg5w7vykg36czquwdseajl3rx07t0sdk4y6m96wfcep7k33gqpnu3x47yf
# (Set a NEW passphrase — DIFFERENT from YubiKey PIN)
gopass show v4/domains/d000/identity/ise/ise-01 (4)
| 1 | Verify current identity — shows the recipient public key. Confirm before removing. |
| 2 | rm takes the recipient as argument — not the identity token. Removes the encrypted keyring entry. |
| 3 | Re-add with the same identity token and recipient but a new keyring passphrase. The identity pointer is unchanged — only the scrypt encryption wrapping it changes. Existing v4 entries remain accessible. |
| 4 | Test the full chain: new keyring passphrase (first prompt) → YubiKey PIN (second prompt) → physical touch → decrypted secret. All three factors must succeed. |
Credential Storage
Store the keyring passphrase in v3 (GPG-protected) — ironic but practical. v3 is your working store and the keyring passphrase is needed to access v4:
gopass edit v3/domains/d000/security/gopass-v4-keyring (1)
| 1 | Store the keyring passphrase here. If you forget it, you must gopass age identities rm + gopass age identities add to reset — existing v4 entries remain encrypted and accessible as long as the YubiKey identity (token + recipient) stays the same. |
Recovery Scenarios
| Scenario | Recovery |
|---|---|
Forgot keyring passphrase |
|
Lost YubiKey |
No recovery. The P-256 private key is non-extractable. Must re-encrypt all v4 entries with a new key (backup YubiKey or software age key). This is why the doc says "everything breaks, have a backup plan." |
YubiKey PIN blocked (0 tries) |
Use PUK to unblock: |
Both keyring + PIN known but touch fails |
Physical YubiKey damage. Replace YubiKey, re-generate identity in same slot, re-add to gopass. Existing entries need re-encryption. |
USB Port Root Cause — INC-006 Postmortem
The Real Problem
After hours of debugging AppArmor profiles, gopass keyring corruption, scrypt identity stores, pcscd socket access, /dev/tty permissions, and gopass Go age library limitations — the root cause was a flaky USB port.
The YubiKey CCID interface failed intermittently on USB port 3-1. Moving to a different physical USB-C port resolved all decrypt failures immediately.
What the Flaky Port Did
-
ykman info— worked (HID interface, not CCID-dependent) -
lsusb— showed the device (kernel USB enumeration, not CCID) -
usbguard list-devices— showedallow(policy layer, not transport) -
pcscd— connected to socket successfully (daemon running) -
age-plugin-yubikey— failed at CCID command level ("Could not open YubiKey") -
Gopass — failed with "no identity matched" because the plugin silently failed
The CCID smart card interface has tighter timing and power requirements than HID (keyboard/FIDO). A marginal USB connection passes HID but fails CCID — the diagnostic tools all reported success because they use HID, not CCID.
What Actually Fixed It
# Moved YubiKey from USB port 3-1 to a different USB-C port
# Immediately:
gopass show v4/test/agent
# Enter secret: <keyring passphrase>
# waiting on yubikey plugin...
# Secret: v4/test/agent
# agent-test
Working Configuration (Confirmed 2026-06-28)
| Component | State |
|---|---|
YubiKey |
5C NFC, serial 31311804, FW 5.7.1, different USB-C port |
AppArmor |
|
AppArmor |
|
USBGuard |
Rule 59: |
gopass config |
|
gopass age agent |
Running, unlocked with keyring passphrase |
gopass identity |
|
Store recipient |
|
Decrypt Chain (Working)
gopass show v4/path/to/secret
│
├── gopass decrypts scrypt keyring (~/.config/gopass/age/identities)
│ → prompts: "Enter secret:" (keyring passphrase)
│
├── gopass extracts AGE-PLUGIN-YUBIKEY identity token
│
├── gopass calls age decrypt internally (Go library)
│ → invokes age-plugin-yubikey subprocess
│ → plugin connects to pcscd → pcscd talks to YubiKey via CCID
│ → prompts: YubiKey PIN
│ → prompts: physical touch
│
└── decrypted secret returned
Lessons
-
USB port quality matters for CCID — HID works on marginal connections, CCID doesn’t. If
ykman infoworks butage-plugin-yubikeydoesn’t, try a different port before debugging software. -
AppArmor fixes were still necessary — the profile changes for
ageandgopasswere real blockers. The USB port was the final blocker, not the only one. -
stracecan’t catch USB transport issues — the syscalls succeed (socket connect returns 0), the failure happens at the CCID protocol level inside the pcscd ↔ YubiKey communication. -
Agent mode is required —
gopass config age.agent-enabled true+gopass age agent start+gopass age agent unlockeliminates the keyring decrypt race condition. -
Document the working USB port — physical infrastructure matters. Label the port or note it.
dsource v4 Integration — Validated 2026-06-28
The Pipeline
PASS=$(gopass show v4/domains/d000/identity/ise/ise-01 | yq -r '.credentials.cli.password') (1)
sshpass -p "$PASS" ssh ise-01 'show version' 2>&1 | head -5 (2)
unset PASS (3)
| 1 | gopass show v4/… triggers the full decrypt chain: keyring passphrase → YubiKey PIN → physical touch → age decrypt → YAML output. yq -r extracts the bare password string. The credential never touches disk. |
| 2 | sshpass feeds the password to SSH non-interactively. The ISE CLI responds with show version output. The password exists only in the shell variable $PASS — not in command history, not in /proc/$$/environ after unset. |
| 3 | unset PASS clears the variable from shell memory. Belt-and-suspenders — the variable would be cleared on shell exit anyway, but explicit cleanup is the standard. |
Validated Output
Enter secret: ← keyring passphrase
waiting on yubikey... ← PIN + touch
admin connected from 10.50.1.204 using ssh on ise-01
Credential Flow — Zero Disk Exposure
YubiKey P-256 private key (hardware silicon)
→ age-plugin-yubikey decrypts .age file
→ gopass outputs YAML to stdout (pipe, not file)
→ yq extracts .credentials.cli.password (pipe)
→ $PASS shell variable (memory only)
→ sshpass feeds to SSH (stdin, not argv)
→ SSH tunnel (encrypted)
→ ISE CLI authenticated
→ unset PASS (memory cleared)
At no point does the password exist as a file on disk. The only exposure is the shell variable between assignment and unset — and that’s in process memory, not filesystem.
Next Steps
-
Update
dsourceprofiles to preferv4/paths when available -
Migrate remaining v3 SSH credentials to v4 (17 entries)
-
Add
gopass age agent startto login shell (.zprofileor systemd user unit) -
Test with
sshpasspipelines for switch, WLC, and other NADs -
Batch v3→v4 migration session (357 entries, weekend project)
Gopass v4 Remote Setup — Validated 2026-06-28
Repository Creation via API
ds d000 dev/app (1)
# GitHub
gh repo create gopass-v4 --private (2)
# GitLab
curl --header "PRIVATE-TOKEN: $GITLAB_PAT" \ (3)
--data "name=gopass-v4&visibility=private" \
"https://gitlab.com/api/v4/projects"
# Gitea
curl -X POST \
-H "Authorization: token $GITEA_TOKEN" \ (4)
-H "Content-Type: application/json" \
-d '{"name":"gopass-v4","private":true}' \
"https://gitea-01.inside.domusdigitalis.dev:3000/api/v1/user/repos"
| 1 | ds (alias for dsource) loads API tokens from ~/.secrets/environments/domains/d000/dev/app.env.age. Check available tokens: env | grep -iE 'GITLAB|GITEA|GIT.*TOKEN' | awk -F= '{print $1"=<REDACTED>"}' |
| 2 | gh CLI for GitHub — simplest, no curl needed. --private creates a private repo. |
| 3 | GitLab uses $GITLAB_PAT (Personal Access Token), NOT $GITLAB_TOKEN. The PAT needs api scope for project creation. If 401 — token expired or wrong scope. |
| 4 | Gitea uses $GITEA_TOKEN. The response is verbose JSON — pipe through | jq '.html_url' for clean output. |
Multi-Remote Push Pattern
git -C ~/.local/share/gopass/stores/v4 remote add origin git@github.com:EvanusModestus/gopass-v4.git (1)
git -C ~/.local/share/gopass/stores/v4 remote set-url --add --push origin git@github.com:EvanusModestus/gopass-v4.git (2)
git -C ~/.local/share/gopass/stores/v4 remote set-url --add --push origin git@gitlab.com:EvanusModestus/gopass-v4.git
git -C ~/.local/share/gopass/stores/v4 remote set-url --add --push origin git@gitea-01:evanusmodestus/gopass-v4.git
| 1 | remote add sets the fetch URL — GitHub is the primary fetch source. |
| 2 | --add --push appends additional push URLs to the same remote name. One git push goes to all three. One git fetch comes from GitHub only. |
Branch Rename + Initial Push
git -C ~/.local/share/gopass/stores/v4 branch -m master main (1)
git -C ~/.local/share/gopass/stores/v4 push -u origin main (2)
| 1 | gopass git init defaults to master. Rename to main before first push. |
| 2 | -u sets upstream tracking. Pushes to all three remotes in sequence — prompts for SSH passphrase for each. Use gopass show v4/domains/d000/identity/ssh/github | yq -r '.passphrase' | wl-copy to copy passphrase to clipboard before pushing. |
Verified State
git -C ~/.local/share/gopass/stores/v4 remote -v (1)
gopass sync (2)
| 1 | Verify: one fetch (GitHub), three push (GitHub + GitLab + Gitea). |
| 2 | All four stores sync clean — root (no git), v2 (OK), v3 (OK), v4 (OK). |
origin git@github.com:EvanusModestus/gopass-v4.git (fetch)
origin git@github.com:EvanusModestus/gopass-v4.git (push)
origin git@gitlab.com:EvanusModestus/gopass-v4.git (push)
origin git@gitea-01:evanusmodestus/gopass-v4.git (push)
API Token Validation
GH_PAGER= gh repo view EvanusModestus/gopass-v4 --json url --jq '.url' (1)
curl -s -H "Authorization: token $GITEA_TOKEN" "https://gitea-01.inside.domusdigitalis.dev:3000/api/v1/repos/evanusmodestus/gopass-v4" | jq '.html_url' (2)
curl -s --header "PRIVATE-TOKEN: $GITLAB_PAT" "https://gitlab.com/api/v4/user" | jq '.username' (3)
| 1 | GH_PAGER= disables the pager — gh opens less by default which hangs in pipelines. |
| 2 | Gitea API — confirmed working, repo exists. |
| 3 | GitLab PAT returned null — token expired or lacks api scope. Pending: regenerate PAT on gitlab.com/profile/personal_access_tokens. |
Pending
-
Regenerate GitLab PAT with
apiscope — current token expired/invalid -
Store new GitLab PAT in v4:
gopass insert v4/domains/d000/identity/api/gitlab-pat -
Update
~/.secrets/environments/domains/d000/dev/app.env.agewith new PAT
yubikey-usbguard-block
YubiKey USBGuard Block — INC-004
Fourth security layer incident. USBGuard policy blocks the YubiKey’s CCID interface (0b:00:00), preventing pcscd and ykman from detecting the smart card. Same diagnostic pattern as INC-001/003 — misleading application error, real cause one layer deeper.
Symptoms
-
ykman info→ERROR: No YubiKey detected! -
lsusbseesYubico.com Yubikey 4/5 OTP+U2F+CCID— kernel detects USB -
dmesgshowsProduct: YubiKey OTP+FIDO+CCID— enumeration succeeds -
pcscdstarts then auto-exits — no readers found -
AppArmor has no
pcscdorccidprofile — not the blocker -
USB permissions
crw-rw----root — correct
Root Cause — USBGuard blocking the device
sudo usbguard list-devices | grep -i yubi (1)
| 1 | list-devices shows the current policy decision for each USB device. The key field is the first word after the device number: allow, block, or reject. |
58: block id 1050:0407 serial "" name "YubiKey OTP+FIDO+CCID" hash "+yHSjnnIMzjrazEwqcEGVKmguy5xWgoVqqAQUnLyL5E=" parent-hash "Kc8J9gPb2yey1PIXEe9+4eCOuAHxcR7EsZlau6dOQZM=" via-port "3-1" with-interface { 03:01:01 03:00:00 0b:00:00 } with-connect-type "hotplug"
Three interfaces on the device:
| Interface | Class | Function |
|---|---|---|
|
HID Keyboard |
OTP — one-time password (touch → keystrokes) |
|
HID Generic |
FIDO/U2F — WebAuthn, SSH resident keys |
|
Smart Card (CCID) |
PIV, OpenPGP, age-plugin-yubikey — this is what pcscd needs |
USBGuard blocks the entire device. pcscd cannot open the CCID interface. ykman cannot communicate via CCID. The OTP and FIDO interfaces are also blocked, but those use the HID kernel driver directly — they may or may not work depending on the USBGuard policy for HID.
Fix — Allow YubiKey in USBGuard
Step 1 — Verify before
sudo usbguard list-devices | grep -i yubi
# Expected: "block"
Step 2 — Allow the device (temporary — until re-plug)
sudo usbguard allow-device 58 (1)
| 1 | 58 is the device number from list-devices. This allows the device for the current session. Re-plugging the YubiKey will block it again unless a permanent rule exists. |
Step 3 — Verify ykman
ykman info (1)
| 1 | Should now show device type, serial number, firmware version, and enabled applications (OTP, FIDO2, PIV, OpenPGP, OATH). |
Step 4 — Make it permanent
sudo usbguard generate-policy | grep -i yubi (1)
sudo usbguard append-rule 'allow id 1050:0407 name "YubiKey OTP+FIDO+CCID" with-interface { 03:01:01 03:00:00 0b:00:00 } with-connect-type "hotplug"' (2)
sudo usbguard list-rules | grep -i yubi (3)
| 1 | Generate a policy line from the current device state — verify the syntax before committing |
| 2 | append-rule adds a permanent allow rule to /etc/usbguard/rules.conf. The rule matches by vendor:product ID (1050:0407 = Yubico) AND interface classes — specific enough to not allow arbitrary USB devices. |
| 3 | Verify the rule was persisted |
Step 5 — Verify after re-plug
Physically remove and re-insert the YubiKey, then:
sudo usbguard list-devices | grep -i yubi (1)
ykman info (2)
| 1 | Should now show allow instead of block |
| 2 | Should show full device info |
Pattern — Fourth Security Layer Incident
| Incident | Security Layer | Application | Blocked Resource |
|---|---|---|---|
INC-001 (2026-06-25) |
AppArmor |
|
|
INC-002 (2026-06-07) |
systemd |
|
wired 802.1X (service not enabled) |
INC-003 (2026-06-27) |
AppArmor |
|
|
INC-004 (2026-06-28) |
USBGuard |
|
YubiKey smart card interface |
The pattern: security layer does its job correctly (blocking unauthorized access), but the error surfaces as a misleading application-level message (No YubiKey detected, not a git repository, trust anchors). The diagnostic is always the same — go one layer deeper than the application error.
std-025-compliance
STD-025 AsciiDoc Authoring Standard — Rollout
Created STD-025 (AsciiDoc Authoring Conventions) and rolled it out across 12 projects spanning education, infrastructure, and work tiers.
What STD-025 defines
-
14 admonition roles — 5 standard + 9 custom CSS roles in
domus-asciidoc-build -
Code block pattern — prose → callouts →
[IMPORTANT.concept]→[TIP.terminal] -
Attribute discipline — all tool names, paths, references as attributes
-
Diagram conventions — source in
diagrams/, SVGs inimages/, pre-render to SVG -
Progress tables —
[NOTE.progress]with teal gradient headers,N/10status text -
Preface blocks —
[NOTE.mission]with compass icon -
Conditional visibility —
ifdef::show-std[]for author-only content
CSS admonition roles added to domus-asciidoc-build
| Role | Icon | Color |
|---|---|---|
|
fa-terminal |
Teal |
|
fa-pencil |
Amber |
|
fa-search |
Indigo |
|
fa-book |
Slate |
|
fa-cogs |
Steel blue |
|
fa-compass |
Warm gold |
|
fa-tasks |
Cool teal |
|
fa-bug |
Rose |
|
fa-key |
Deep blue |
Projects brought to compliance
|
STD-025 Compliance Status
|
How to open any project
# List all discoverable projects
cd ~/atelier/_bibliotheca/domus-captures && make build-data-list
# Build a specific project
make build-data PROJ=<slug>
# Build with private attributes (d000)
make build-data PROJ=ise-api ENV=env-d000
# Build d001 projects (decrypt first)
d001 open <slug>
make build-data PROJ=<slug> TIER=d001 ENV=env-d001
# verify, then:
d001 close <slug>
# Open in browser
firefox data/d000/<category>/<project>/output/<project>.html &
References
-
STD-025:
docs/modules/ROOT/pages/standards/STD-025-asciidoc-authoring.adoc -
STD-025 partial:
docs/modules/ROOT/partials/standards/STD-025-asciidoc-authoring.adoc -
Education standard:
data/d000/education/EDUCATION-ASCIIDOC-STD.adoc -
CSS roles:
~/atelier/_bibliotheca/domus-asciidoc-build/docinfo/css-light-cyan/admonitions.css -
Companion to: STD-024 (AsciiDoc Build Toolchain)
vault-token-rotation
Vault SSH CA Cert Renewal + Key Rotation
Vault-signed SSH certificate expired June 22 (8-hour TTL). Key passphrase was set with gopass pwgen 32 during a 48-hour no-sleep session and never stored. Passphrase rotated, stored in gopass v4, cert re-signed, all three raft nodes verified.
Vault SSH CA Certificate Renewal — Validated 2026-06-28
The Problem
ssh-keygen -L -f ~/.ssh/id_ed25519_vault-cert.pub 2>&1 | grep Valid (1)
# Valid: from 2026-06-21T19:16:05 to 2026-06-22T03:16:35 ← EXPIRED 6 days ago
| 1 | Vault-signed SSH certs have an 8-hour TTL. When expired, SSH falls back to key-based auth which vault-01/02/03 reject — they require CA-signed certificates. ykman info and ssh modestus-p50 work because those hosts accept the bare key, not the cert. |
The Script
ds d000 dev/vault (1)
~/.local/bin/vault-ssh-sign (2)
| 1 | Load Vault credentials — $VAULT_ADDR, $VAULT_TOKEN, $VAULT_CACERT. The script needs API access to the Vault cluster. |
| 2 | vault-ssh-sign — located in ~/atelier/_projects/personal/dots-quantum/bin/.local/bin/. Calls vault write -field=signed_key ssh/sign/domus-client with the correct principals and writes the cert to ~/.ssh/id_ed25519_vault-cert.pub. |
[INFO] Using specified VAULT_ADDR: https://vault-01.inside.domusdigitalis.dev:8200
[INFO] Signing SSH key with Vault CA...
[INFO] Key: /home/evanusmodestus/.ssh/id_ed25519_vault.pub
[INFO] Principals: Administrator,domus\Administrator,adminerosado,admin,
ansible,evanusmodestus,gabriel,root,u0_a361
Enter passphrase for /home/evanusmodestus/.ssh/id_ed25519_vault:
Identity added: /home/evanusmodestus/.ssh/id_ed25519_vault (vault-signed-20260219)
Certificate added: /home/evanusmodestus/.ssh/id_ed25519_vault-cert.pub
Lifetime set to 08:04:22
[INFO] Certificate signed successfully:
Type: ssh-ed25519-cert-v01@openssh.com user certificate
Valid: from 2026-06-28T21:17:38 to 2026-06-29T05:18:08
Verification
ssh vault-01 "whoami" (1)
ssh vault-02 "whoami" (2)
ssh vault-03 "whoami" (3)
| 1 | evanusmodestus — vault-01 accepts the fresh cert |
| 2 | evanusmodestus — vault-02 accepts |
| 3 | evanusmodestus — all three raft nodes accessible |
Where Things Live
| Component | Location |
|---|---|
Signing script |
|
Test script |
|
Architecture docs |
|
Runbook |
|
D2 diagram |
|
Private key |
|
Certificate |
|
SSH config |
|
Vault credentials |
|
Key passphrase |
|
Daily Workflow
ds d000 dev/vault (1)
export SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh (2)
export SSH_ASKPASS_REQUIRE=force
~/.local/bin/vault-ssh-sign (3)
ssh vault-01 "hostname" (4)
dsu (5)
| 1 | Load Vault API credentials into environment |
| 2 | Set askpass to gopass extractor — vault-ssh-sign gets the passphrase automatically via YubiKey (PIN + touch). No clipboard step. |
| 3 | Run signing script — passphrase supplied by askpass. Cert valid 8 hours. |
| 4 | Verify — should connect without "Permission denied" |
| 5 | dsu — unload Vault vars from environment when done. Credential hygiene. |
gabriel-lua-gmod
Gabriel — Lua Programming via Garry’s Mod
Dad deployed a custom HUD addon to Gabriel’s GMod installation via SSH from the P16g. The addon replaces the default HUD with a custom health bar (color-changing), armor bar, weapon display, kill counter, and title text. Built in 5 incremental parts — each adds one feature so Gabriel sees it work before moving on. Every line has callouts explaining both the GMod concept and the real programming concept it teaches.
What is Lua?
Lua is a real programming language. It was designed in Brazil in 1993 and is used inside hundreds of games and applications because it is small, fast, and easy to embed. Garry’s Mod uses Lua for all of its addons and gamemodes. Roblox uses a variant called Luau. World of Warcraft uses Lua for its entire addon system.
This is not a toy language or a simplified "kid" version of programming. When you write Lua in GMod, you are writing the same kind of code that professional game developers write. The concepts — variables, functions, conditionals, loops, tables — transfer directly to Python, JavaScript, C, and every other language.
Console Access and Addon Structure
To open the GMod developer console, press ~ (tilde). If it does not open, go to Options > Keyboard > Advanced and enable "Enable Developer Console." The console shows print() output and error messages — this is how you debug.
garrysmod/addons/
└── my_first_addon/
└── lua/
└── autorun/
└── client/
└── my_hud.lua <-- your code goes here
GMod automatically loads any .lua file inside addons/<name>/lua/autorun/client/ when the game starts. The client folder means this code runs on your machine (drawing your HUD), not on the server.
Part 1 — Your Name on Screen
hook.Add("HUDPaint", "GabrielHUD", function() (1)
draw.SimpleText( (2)
"// GABRIEL'S WORLD //", (3)
"DermaLarge", (4)
ScrW() / 2, 30, (5)
Color(255, 215, 0), (6)
TEXT_ALIGN_CENTER
)
end)
| 1 | hook.Add("HUDPaint", …) — run this function every frame (60+ fps). Event-driven programming. |
| 2 | draw.SimpleText() — draws text on screen. 6 args: text, font, x, y, color, alignment. |
| 3 | Change this to anything — your gamertag, a message, your mood. |
| 4 | Built-in font. Try "DermaDefault", "Trebuchet24". |
| 5 | ScrW() / 2 = center of screen. Works on any monitor — responsive design. |
| 6 | Color(R, G, B) — gold. Color(255,0,0) = red. Color(0,255,0) = green. |
Part 1 — deploy just the title
|
|
What just happened
You told GMod: "every frame, draw my name in gold." The game redraws it 60+ times per second. The screen doesn’t remember — it paints fresh every time. This is how ALL game HUDs work. |
Part 2 — Health Bar That Changes Color
local hp = LocalPlayer():Health() (1)
local hpColor (2)
if hp > 75 then (3)
hpColor = Color(0, 255, 100) -- green
elseif hp > 25 then
hpColor = Color(255, 200, 0) -- yellow
else
hpColor = Color(255, 50, 50) -- red!
end
draw.RoundedBox(8, 20, ScrH()-60, (4)
200, 40, Color(0,0,0,180)) -- dark background
draw.RoundedBox(8, 20, ScrH()-60,
200 * (hp / 100), 40, hpColor) (5)
draw.SimpleText("HP: " .. hp, "DermaLarge", (6)
120, ScrH()-55, Color(255,255,255),
TEXT_ALIGN_CENTER)
| 1 | LocalPlayer() = YOU. Ask it questions: :Health(), :Armor(), :Nick(). Object-oriented programming. |
| 2 | Variable — a container that holds a value. local means it exists only here. |
| 3 | if/elseif/else — computer makes a decision. Same in Python, JS, C. Conditional logic. |
| 4 | draw.RoundedBox(radius, x, y, width, height, color) — the dark background. |
| 5 | 200 * (hp / 100) — THE KEY. Health 100 = full bar. Health 50 = half. Health 10 = sliver. Linear interpolation — math making your game visual. |
| 6 | "HP: " .. hp — .. joins text. String concatenation. Python: f"HP: {hp}". Bash: "HP: $hp". |
Part 2 — deploy title + health bar
|
|
Decision trees
The |
Part 3 — Armor Bar
local armor = LocalPlayer():Armor() (1)
if armor > 0 then (2)
draw.RoundedBox(8, 230, ScrH()-60, 200, 40, Color(0,0,0,180))
draw.RoundedBox(8, 230, ScrH()-60, 200*(armor/100), 40,
Color(50, 150, 255)) (3)
draw.SimpleText("ARMOR: "..armor, "DermaLarge",
330, ScrH()-55, Color(255,255,255), TEXT_ALIGN_CENTER)
end
| 1 | :Armor() — same pattern as :Health(). Another method on the player object. |
| 2 | Only draw if you have armor. Guard clause — skip work that isn’t needed. |
| 3 | Bright blue — armor is traditionally blue in games. |
Part 3 — deploy title + health + armor
|
Part 4 — Weapon + Kill Counter
local wep = LocalPlayer():GetActiveWeapon() (1)
if IsValid(wep) then (2)
draw.SimpleText(wep:GetPrintName(), (3)
"DermaLarge", ScrW()-30, ScrH()-55,
Color(200,200,200), TEXT_ALIGN_RIGHT)
end
draw.SimpleText("KILLS: "..LocalPlayer():Frags(), (4)
"DermaDefault", ScrW()-30, 30,
Color(255,100,100), TEXT_ALIGN_RIGHT)
| 1 | GetActiveWeapon() — the weapon you’re holding. An object with its own methods. |
| 2 | IsValid(wep) — safety check. You might be dead. Without this = crash. Null checking. |
| 3 | wep:GetPrintName() — asks the weapon for its name. : = method call on an object. |
| 4 | Frags() — kill count. Updates live every kill. |
Part 5 — Hide Default HUD
hook.Add("HUDShouldDraw", "HideDefaultHUD", (1)
function(name)
local hide = { (2)
["CHudHealth"] = true,
["CHudArmor"] = true,
["CHudAmmo"] = true,
}
if hide[name] then return false end (3)
end)
print("[GABRIEL] Custom HUD loaded!") (4)
| 1 | GMod asks "should I draw this default element?" Your function answers. |
| 2 | A table — Lua’s dictionary. Keys = HUD element names to hide. |
| 3 | Return false = don’t draw it. Elements NOT listed draw normally. |
| 4 | print() — outputs to console. See this = script loaded. Debugging. |
Full Deploy
Full script — deploy the complete HUD (all 5 parts)
|
Challenges
|
Start easy — each teaches something new
|
What You’re Learning
| GMod | Real Programming |
|---|---|
|
Event-driven — code responds to events |
|
RGB — same as CSS, Photoshop |
|
Responsive design — works on any screen |
|
Conditionals — same in Python, JS, C |
|
OOP — objects have methods |
|
Linear interpolation — mapping ranges |
|
String concatenation |
|
Null checking — prevent crashes |
|
Hash tables — Python dict, JS object |
|
Debugging — first tool every dev learns |
|
This is real programming
You wrote an event-driven, responsive, object-oriented program with conditionals, string ops, null checking, and data structures. Every concept transfers to Python, JavaScript, C. This is where many developers started. |
Gabriel’s Arsenal — 6 Custom Weapons
Deployed 6 weapons to Gabriel’s GMod installation via lua/autorun/gabriel_arsenal.lua. Uses weapons.Register() instead of the addon SWEP system — bypasses snap filesystem mounting issues.
Deployment History
| Attempt | Method | Result |
|---|---|---|
v1 — addon folder |
|
|
v2 — autorun registration |
|
Bypasses addon system entirely. Single file, 6 weapons, auto-loads on startup. |
|
Snap sandbox lesson
GMod installed via Snap has filesystem isolation. The addon folder exists and files are readable ( |
Weapons
| Console Command | Weapon | Features |
|---|---|---|
|
Tactical Rifle |
Auto fire, iron sights (right-click zoom), tracers, recoil, custom ammo HUD |
|
Gravity Cannon |
Left-click launches props, right-click pulls them — physics force scaled by mass |
|
Explosive Crossbow |
Fires bolts that detonate after 2 seconds — 250 radius blast |
|
Lightning Gun |
Hold to zap — continuous beam, 800 unit range, electric effects |
|
Airstrike Marker |
Aim + click = warning bell → 5 sequential explosions at target |
|
Railgun |
One-shot 500 damage sniper, 2 sec reload, right-click 15 FOV scope |
Deploy Command
Deploy all 6 weapons from dad’s machine
|
File Location
/home/gabriel/snap/steam/common/.local/share/Steam/
steamapps/common/GarrysMod/garrysmod/
lua/autorun/gabriel_arsenal.lua ← all 6 weapons in one file
How Gabriel Uses Them
-
Restart GMod completely (quit + reopen from Steam)
-
Open console:
~ -
Look for
[GABRIEL] Arsenal loaded! 6 weapons ready. -
Type:
give gabriel_rifle(or any weapon name) -
Or open Q menu → "Gabriel’s Arsenal"
js-yaml-dependabot-dismiss
js-yaml Dependabot Alert — Dismissed (Tolerable Risk)
GitHub Dependabot alert #10 on domus-captures: js-yaml ⇐4.1.1 quadratic-complexity DoS via repeated merge key aliases (GHSA-h67p-54hq-rp68).
Assessment
| Field | Value |
|---|---|
CVE |
GHSA-h67p-54hq-rp68 |
Severity |
Moderate (DoS, not RCE) |
Package |
|
Locked by |
|
Fix available |
No — Antora has not released a patched version |
Attack vector |
Crafted YAML with repeated merge key aliases |
Exposure |
None — all YAML input is author-controlled (antora.yml, antora-playbook.yml). No untrusted input path. |
Decision
|
Why this is tolerable
Dismissed as tolerable risk. The vulnerability requires an attacker to feed crafted YAML to the parser. In domus-captures, all YAML is authored by the project owner — there is no user-submitted, API-ingested, or externally-sourced YAML. The DoS has no path to trigger. Re-evaluate when Antora releases a version with |
Dismiss Command
gh api repos/EvanusModestus/domus-captures/dependabot/alerts/10 \
-X PATCH \
-f state=dismissed \
-f dismissed_reason="tolerable_risk" \
-f dismissed_comment="DoS via crafted YAML — all YAML input is author-controlled. No untrusted input path. Waiting for Antora upstream fix."
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
ersfunction. -
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-researchto 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 |
— |
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 |
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
-
docs.domusdigitalis.dev - Private documentation hub
-
docs.architectus.dev - Public portfolio site
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 |
Active Tracks (Focus)
-
Don Quijote - Primera Parte
Skills Mastery (Critical)
-
Regex Mastery - 10-module curriculum
-
AsciiDoc Docs - Documentation format
-
Antora Docs - Documentation pipeline
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 |
|---|---|---|---|---|
Instituto Cervantes / UNAM / Salamanca |
Q2 2026 |
ACTIVE |
Computer-based, faster results - take FIRST |
|
Q3/Q4 2026 |
PLANNED |
After SIELE success, harder exam |
||
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:
-
Read chapter in original Spanish
-
Write personal analysis/understanding en espanol
-
AI review for grammar, vocabulary, register
-
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 |
Active |
Validate, harden, improve |
|
Architectus |
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
gh repo create <name> --private --source . --remote origin --push
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
|
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
git log --oneline -- $(find . -name "*.adoc" -type f -newermt "$(date +%F)")
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
gh repo list --limit 100 --json name,description \
| jq -r '.[] | select(.name | test("domus|antora|asciidoc"; "i")) | "\(.name)\t\(.description)"'
gh repo list --limit 100 --json name,description,updatedAt \
| jq -r 'sort_by(.updatedAt) | reverse | .[:20] | .[] | "\(.updatedAt[:10])\t\(.name)\t\(.description)"'
gh repo list --limit 100 --json name,diskUsage \
| jq -r '.[] | "\(.diskUsage)\t\(.name)"' | sort -rn | head -10
gh repo clone EvanusModestus/<repo-name> ~/atelier/_bibliotheca/<repo-name>
find & grep
find . -name "*.adoc" -type f -newermt "$(date +%F)" | sort
-mtime 0 means "last 24 hours", not "today". -newermt "$(date +%F)" compares against midnight — exact.
|
find . -iname "*mschap*" -type f | sort
find . -type f \( -iname "*ise*" -o -iname "*mschap*" \) | sort
find . -type f -iregex '.*\(ise\|mschap\).*'
find . -type f -iname "*meeting*" \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-not -path "*/build/*"
find .drafts -type f -printf '%T@ %Tc %p\n' | sort -rn | awk '{$1="";print}' | head -3
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
grep -rn -E 'git init|gh repo create' docs/ --include='*.adoc' -B2 -A2
Search codex by content — which files contain a command?
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.
find docs/modules/ROOT -name "powershell" -type d \
-exec sh -c 'echo "$1: $(find "$1" -type f | wc -l) files"' _ {} \;
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
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.
nvim $(find docs/modules/ROOT -name '*.adoc' -type f \
-exec grep -l 'token.*expire\|oauth.*refresh' {} \;)
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
grep -rl 'commands/shell' docs/modules/ROOT/partials/
grep -rl 'quick-commands' docs/modules/ROOT | wc -l
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
find docs/modules/ROOT -name "*urgent.adoc*" -type f
find docs/modules/ROOT -name "*morning.adoc*" -type f
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.
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.
grep -rn --include='*.adoc' -c 'sanchuelo' . | grep -v ':0$'
grep -rl --include='*.adoc' -i 'sanchuelo' ~/atelier/_bibliotheca/ | sort
grep -rn --include='*.adoc' -i -B1 -A1 'sanchuelo' ~/atelier/_bibliotheca/domus-captures/
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.txt' --include='*.adoc' | sort
find ~/atelier/_bibliotheca/ -type f \( -name '*.adoc' -o -name '*.txt' \) -print0 \
| xargs -0 grep -li 'sanchuelo' | sort
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
# Find files — filenames only
grep -rl 'build-antora-page' docs/modules/ROOT/partials/worklog/
# 3 lines of context around each hit
grep -rn -C3 'build-antora-page' docs/modules/ROOT/partials/worklog/
grep -rPn 'theme\s+(light-cyan|catppuccin|mocha)' docs/modules/ROOT/partials/
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
-exec grep -l 'build-antora-page\|build-adoc\|--theme\|pdf' {} \;
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
-exec awk '/build-antora-page|build-adoc|--theme/{print FILENAME": "$0}' {} \;
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.
find docs/modules/ROOT/partials/worklog -name '*.adoc' -exec \
sed -n '/\[source,bash\]/,/^----$/{/build-antora-page/p}' {} +
grep -rn 'build-antora-page\|build-adoc' data/ --include='*.adoc'
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
grep -P '(@\w+|^From:.*<)' comms.adoc
grep -nP '\d{1,2}/\d{1,2}/\d{2,4}|20\d{2}-\d{2}-\d{2}' comms.adoc
grep -niP '(I can |I will |I.ll |we will |we.ll )' comms.adoc
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.
|
# 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.
# 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-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.
# 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.
# 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.
# 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]}'
# 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.
# 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.
# 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.
# 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
# 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.
# 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.
# 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
# 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.
# 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.
# 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
# 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.
# 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
awk '{print $2}' file.txt
awk -F: '{print $1, $3}' /etc/passwd
awk '/\[source,json\]/{getline; if ($0 ~ /^----/) {p=1; next}} p && /^----/{p=0; next} p' file.adoc
awk '{printf "%-30s %s\n", $1, $2}' file.txt
sed — stream editing
# 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
sed -n '10,20p' file.txt
sed — line-targeted replacement (verify-before / change / verify-after)
# 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.
awk 'NR==1218 || NR==1760' zsh/.zshrc
# 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
# 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.
# 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
curl -s localhost:8080/stats | jq '.stats.total_files'
jq '.results[] | select(.category == "standards")' response.json
jq -r '.[] | [.title, .path] | @tsv' response.json | column -t -s $'\t'
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 ( |
pipe directly |
arguments ( |
|
-I{} placeholdermkdir -p /tmp/adoc-backup-$(date +%F) && \
find . -name "*.adoc" -type f -newermt "$(date +%F)" | \
xargs -I{} cp {} /tmp/adoc-backup-$(date +%F)/
-P4 runs 4 at a timefind .drafts -name "*.adoc" -type f | xargs -P4 -I{} asciidoctor -o /dev/null {}
find . -name "*.adoc" -type f -print0 | xargs -0 wc -l
Process substitution — <(cmd) treats output as a file
diff <(grep '|' partials/trackers/work/adhoc/carryover.adoc | head -20) \
<(git show HEAD~1:partials/trackers/work/adhoc/carryover.adoc | grep '|' | head -20)
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
nvim "$(find data/ -name '*.adoc' -type f -printf '%T@ %p\n' | sort -rn | awk 'NR==1{print $2}')"
wc -l $(find docs/modules/ROOT -path '*mschapv2*' -name '*.adoc' -type f)
Conditional execution — capture, test, act
files=$(find .drafts -name 'in*' -type f) && [ -n "$files" ] && nvim $files
files=$(grep -rl '\[ \]' .drafts/*.adoc) && [ -n "$files" ] && nvim $files
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
--- as invalid optionprintf '---\n\n'
--- as dataprintf '%s\n\n' '---'
Kill stuck SSH sessions
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2, $9}'
lsof -i TCP -n -P | awk '/ssh.*kvm-01.*ESTABLISHED/ {print $2}' | sort -u | xargs kill
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
cat <<'EOF'
Line 1
Line 2
EOF
git commit -m "$(cat <<'EOF'
feat: add new feature
Multi-line description here.
EOF
)"
API & curl/jq
domus-api — Documentation System REST API
cd ~/atelier/_projects/personal/domus-api && uv run uvicorn domus_api.main:app --host 0.0.0.0 --port 8080
curl -s localhost:8080/ | jq
curl -s 'localhost:8080/search?q=mandiant' | jq
curl -s 'localhost:8080/search?q=mandiant' | jq '.results[] | {path, title, match_count}'
curl -s 'localhost:8080/pages?category=standards' | jq
curl -s localhost:8080/attributes | jq
GitHub API
gh search code "vault seal" --owner EvanusModestus --json repository,path,textMatches |
jq '.[] | {repo: .repository.full_name, file: .path, match: .textMatches[].fragment}'
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)
bat docs/modules/ROOT/pages/2026/04/WRKLOG-$(date +%Y-%m-%d).adoc
bat docs/modules/ROOT/partials/trackers/work/priorities/current.adoc
bat docs/modules/ROOT/partials/trackers/work/adhoc/carryover.adoc
bat docs/modules/ROOT/partials/projects/mandiant-remediation/summary.adoc
Search and discovery
grep -rl "MSCHAPv2" docs/modules/ROOT/ --include="*.adoc" | sort
grep -rn "pattern" docs/modules/ROOT/partials/codex/ --include="*.adoc" -B1 -A3
ls -1 docs/modules/ROOT/pages/2026/04/WRKLOG-*.adoc
Tracker aging — calculate days from origin
echo $(( ($(date +%s) - $(date -d "2026-03-09" +%s)) / 86400 ))
Encrypted data access (d001)
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
| bat --language asciidoc
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
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
d000 build p1-cap-038/texto-anotado html --variant light-cyan
d000 build p1-cap-038/texto-anotado pdf --theme light-cyan
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
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 &
lp data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf
d000 build annotated-text pdf --theme light-cyan
d000 build lpl-study/notas/texto-anotado pdf --theme light-cyan
d000 build de-oratore/libro-i/texto-anotado html --variant light-cyan
Available themes
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
~/atelier/_bibliotheca/domus-asciidoc-build/docinfo/compose.sh --list
# light dark catppuccin royal light-cyan
ISE & Network Ops
ISE ERS API — endpoint CRUD
export ISE_HOST="{ise-ip}" ISE_USER="admin" ISE_PASS="$(gopass show -o ise/admin)"
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/identitygroup" \
-H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.resources[].name'
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
openssl x509 -in {cert-dir}/client.pem -text -noout | head -30
openssl x509 -in {cert-dir}/client.pem -enddate -noout
Network diagnostics
ss -tlnp | grep -E ':{port-https}|:{port-ssh}|:{port-ldaps}'
nc -zv {ise-ip} {ise-ers-port}
dig {ise-hostname} +short
ISE eval rotation — backup & restore
# 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>
ssh admin@ise-02.inside.domusdigitalis.dev
show repository nas-01
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
show vrrp
show configuration commands | grep vrrp | grep 'address'
show configuration commands | grep 'firewall zone' | grep 'member'
show dhcp server leases
show arp
show interfaces
CUPS printing — validation & setup
command -v lpstat && echo "CUPS present" || echo "CUPS not installed"
lpstat -r # scheduler running?
lpstat -p -d # printers + default
sudo systemctl enable --now cups # start + persist
lpinfo -v # available backends/URIs
lpinfo -m | grep -i <brand> # available drivers
sudo lpadmin -p <name> -v <uri> -m everywhere -E
lpoptions -d <name>
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
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
Select-Object -First 5 ProcessName, Id,
@{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | Format-Table'
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)
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)
netsh wlan disconnect interface="Wi-Fi"
netsh wlan show networks mode=bssid
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"
SSH from PowerShell
ssh evan@modestus-razer.inside.domusdigitalis.dev
WSL ↔ Windows — Cross-Environment Commands
From zsh (WSL) — control Windows
pwsh -NoLogo -Command 'Get-Date'
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 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'
# 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
# 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
wsl -e bash -c 'grep -rn "Ghost-Sender" ~/atelier/_bibliotheca/domus-captures/docs/'
$result = wsl -e bash -c 'git -C ~/atelier/_bibliotheca/domus-captures log --oneline -5'
$result
Process Management — Windows Side
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'
pwsh -NoLogo -Command 'Get-Process | Where-Object { $_.ProcessName -like "*teams*" } |
Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
Format-Table -AutoSize'
pwsh -NoLogo -Command 'Stop-Process -Name "Teams" -Force -ErrorAction SilentlyContinue'
pwsh -NoLogo -Command 'Stop-Process -Id 12345 -Force'
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'
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
pwsh -NoLogo -Command 'Get-Service | Where-Object { $_.Status -eq "Running" } |
Sort-Object DisplayName | Format-Table Name, DisplayName, Status -AutoSize'
pwsh -NoLogo -Command 'Get-Service -Name "WinRM" | Format-List Name, DisplayName, Status, StartType'
Restart-Service -Name "WinRM" -Force
System Info — Quick Health from zsh
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"'
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
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.
|
pwsh -NoLogo
# Then inside pwsh:
# Connect-ExchangeOnline
# Get-TransportRule | Format-List Name, State
# exit
File Transfer Patterns
# 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/
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
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
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
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
ssh kvm-01 "sudo virsh list --all"
ssh kvm-02 "sudo virsh list --all"
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"
sudo virsh console <vm-name> # Escape: Ctrl+]
ssh kvm-01 "mount | grep nas; ls /mnt/"
Per-project file 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)
echo "$(basename "$d") | ${total} files | ${plain} plaintext"
done
USB-C / Thunderbolt Charging Diagnostics
{
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/'