WRKLOG-2026-06-30

Summary

Tuesday. Fill summary at end of day.

URGENT - All Domains

Carryover Backlog (CRITICAL)

Task Details Origin Days Status

MSCHAPv2 Migration Report

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

2026-04-17

83

P0 - DUE — run report this week

Abnormal Security — ✅ COMPLETE

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

2026-05-07

63

✅ IMPLEMENTED — post-validation pending

SIEM QRadar → Sentinel Migration

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

2026-04-10

90

P0 - ACTIVE — ISE syslog + DCR blocking

Monad Pipeline Evaluation

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

2026-03-11

120

P0 - ACTIVE — ISE integration in progress

Guest Redirect ACL

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

2026-05-12

58

P0 - TODO

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

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

2026-03-12

119

P0 - OVERDUE — schedule immediately

k3s NAT verification

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

2026-03-09

122

P0 - BLOCKING — TRIAGE: schedule or defer

Wazuh indexer recovery

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

2026-03-09

122

P0 - Blocked by k3s

Strongline Gateway VLAN fix

8 devices in wrong identity group (David Rukiza assigned)

2026-03-16

115

P0 - TODO

TCP Clocks deployment

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

2026-04-22

78

P0 - ACTIVE

IoT Dr. Kim — recurring

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

2026-04-15

85

P0 - RECURRING

Murus Portae (WAF) — Phase 0

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

2026-04-16

84

P0 - INVESTIGATING

Vocera EAP-TLS Supplicant Fix

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

2026-03-12

119

P1 - TODO — schedule

ISE MnT Messaging Service

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

2026-03-12

119

P2 - BUNDLE with Patch 10

Professional backlog remains critical. Check Days column for priorities.

BLOCKERS — Fix Immediately

Task Details Origin Days Impact

Z Fold 7 Termux

gopass and SSH not working

2026-03-10

58

BLOCKER — Cannot access passwords on mobile

gopass v3 organization

Inconsistent structure, poor key-value usage

2026-03-20

48

Inefficient password management, no aggregation

Git history scrub — sensitive personal terms

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

2026-04-22

15

SECURITY — sensitive terms in public git history

Runbook: Git History Scrub (d000 Personal Terms)

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

Affected commits: Any commit touching these files:

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

Scrub procedure:

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

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

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

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

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

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

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

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

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

Post-scrub checklist:

  • Backup created before running

  • git filter-repo installed

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

  • Dry-run counts match expectations

  • Filter-repo executed

  • Post-scrub verification shows 0 matches

  • Remotes re-added

  • Force-pushed to all remotes

  • Cloudflare Pages rebuild verified

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

  • Backup removed

URGENT - Requires Immediate Action

Item Details Deadline Status Impact

Housing Search

Granada Hills area - apartments/rooms

TBD

In Progress

Quality of life, commute

2025 Tax — IRS Transcript Review

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

Before attorney meeting

In Progress

Financial — liability exposure. See encrypted D000 case file.

Rack Relocation

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

TBD

Pending

Infrastructure downtime — all services offline during move

D000 Legal Planning

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

Before Jan 2029

Active — escalating

Life transition — see case file for details

Credit Report Review

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

TBD

In Progress

Financial discovery — FL-142 preparation

Gopass Security Audit

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

TBD

Pending

Digital security — pre-filing preparation

Subscription Audit

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

TBD

Pending

Financial — expense documentation

401(k) Enrollment

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

In progress (started 5/4)

In Progress

Financial — support calculation + retirement

URGENT — Performance Review Certifications

Certification Provider Deadline Status Impact

CISSP

ISC² — Certified Information Systems Security Professional

July 12, 2026

ACTIVE — Week 2 of 10 (Project)

Required for performance review. 10-week accelerated plan.

RHCSA 9

Red Hat Certified System Administrator

Q3 2026

ACTIVE — 21-phase curriculum (Project)

After CISSP. Required for performance review.

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

Early Morning - 5:30am

Regex Training (CRITICAL CARRYOVER)

  • Session 3 - Character classes, word boundaries

  • Practice drills from regex-mastery curriculum

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

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

Daily Notes

Triage Status

Item Status Destination

ACME cert project STD-001 audit

Done

acme-cert-compliance/audit-results.adoc — both projects pass

ISE guest portal strategy (sponsor + social login)

In Progress

guest-portal-ise/ — hybrid portal architecture documented

EAP-TEAP home testing (mirror work GPO)

In Progress

eap-teap-home-testing/ — ISE policy set documented

VyOS firewall rules (IoT git + work demo)

In Progress

vyos-firewall-rules/ — permanent git + temporary demo rules

Android Vault cert (Z Fold 7)

In Progress

android-vault-cert/ — PKCS12 workflow documented

Domus infra wireless updates

Pending

domus-infra-updates/ — graduate configs after validation

SSH 3560CX legacy crypto (carryover 06-28)

Pending

00-exceptions.conf patch ready

SSH modestus-p50 (carryover 06-28)

Pending

Needs IP for 80-d000-hosts.conf

gopass v4 standardization (all 10 entries)

In Progress

gopass-v4-standardization/ — github validated, 9 remaining

Raíces Grecolatinas project scaffold

Done

raices-grecolatinas/ — 12 chapters, full content, EDUCATION-ASCIIDOC-STD

acme-cert-compliance

ACME Cert Automation — STD-001 Compliance Audit

Audit Results — Both Cert Projects

Both certificate projects were audited against STD-001 (Project Structure) and STD-025 (Conditional Rendering Security). Both pass.

ise-annual-cert-renewal (d001 + Antora)
Requirement Status Notes

metadata.adoc

Pass

PRJ ID, author, dates, status, category, priority, ISE version, cert type

summary.adoc

Pass

Phase summary with status tracking, cert fields, roles/ports

Partials hold content

Pass

26 partials + 4 audience-specific assemblers

Pages are shells

Pass

index.adoc + phase pages include partials only

Attributes centralized

Pass

attributes.adoc.age (encrypted), subs=attributes+ on code blocks

Nav entry

Pass

Lines 148-159 in nav.adoc — 5 phases + 3 appendices

Diagrams

N/A

Narrative-focused project (no topology diagrams needed)

appendix-issues + appendix-todos

Pass

Both present

ise-acme-cert-automation (d001 only — not published to Antora)
Requirement Status Notes

metadata.adoc

Pass

.age encrypted, PRJ ID, infrastructure requirements

summary.adoc

Pass

Architecture overview, workflow, context, rationale

Partials hold content

Pass

10 partials + 2 Graphviz diagrams + 1 automation script

Root assembler

Pass

ise-acme-cert-automation.adoc.age

Attributes centralized

Pass

attributes.adoc.age (Cloudflare token, LE endpoints, ISE API)

Diagrams

Pass

architecture.dot + vault-auth-flow.dot (Catppuccin Mocha, rendered to SVG)

Nav entry

N/A

d001-only project — correct per STD-006 nav exclusion rule

Phase structure

Pass

phase-01 through phase-04, each separate partial

Next Step — Phase 04 Extension

phase-04-guest-portal.adoc exists but needs content for:

  • ISE guest portal configuration (sponsor portal + social login option)

  • WLAN profile for Domus-Guest SSID

  • Cert binding for portal HTTPS (using ACME-issued Let’s Encrypt cert)

  • Authorization profile for guest VLAN assignment

This work is tracked in the guest-portal-ise concern for today.

guest-portal-ise

ISE Guest Portal — Sponsor + Social Login Strategy

Portal Strategy — Decision Matrix

Three ISE guest portal types evaluated for Domus-Guest SSID:

Portal type comparison
Type How It Works Pros / Cons Verdict

Hotspot

Open access, optional AUP acknowledgment

Simple but zero authentication — anyone in range connects

Rejected — "people smooching on my wireless"

Sponsor Portal

Authorized user (sponsor) approves each guest device. Guest gets time-limited access.

Strong control, audit trail. Overhead: sponsor must be available for every new device.

Use for known visitors — colleagues, family devices you pre-approve

Self-Registration with Social Login

Guest registers via Google/Apple/Facebook OAuth. ISE creates temporary account, assigns guest VLAN.

Authenticated without sponsor bottleneck. Audit trail via social identity. No open access.

Use as default guest onboarding — visitors self-serve, you get identity + logs

Run both portals on the same Domus-Guest SSID:

  1. Default flow: Self-Registration Portal with social login (Google + Apple)

    • Guest connects to Domus-Guest SSID

    • HTTP redirect to ISE self-registration portal

    • Guest authenticates via Google/Apple OAuth

    • ISE creates guest account, assigns VLAN 30 (Guest), time-limited (24h default)

    • RADIUS accounting logs the social identity

  2. Override flow: Sponsor Portal for pre-approved devices

    • You (sponsor) log into ISE sponsor portal

    • Create guest account with MAC + duration

    • Guest device gets immediate access without self-registration

    • Use case: work colleagues visiting to see the lab, family devices

Why social login over simple password

A shared password (WPA2-PSK on guest SSID) gives you network-level authentication but zero identity. You know someone connected — you don’t know who.

Social login gives you:

  • Identity bindinguser@gmail.com tied to MAC address in ISE session logs

  • Accountability — if someone abuses your network, you have a name

  • No password management — no shared secret to rotate, leak, or text to guests

  • Time-limited — accounts auto-expire, no stale credentials

  • Audit trail — ISE reports show who connected, when, how long

The tradeoff: requires internet connectivity for OAuth callback. If your ISE portal can’t reach Google/Apple OAuth endpoints, social login fails. Fallback: sponsor-created account.

ISE License Requirement

Social login requires ISE Advantage license (or legacy Plus). Check current license:

ssh ise-01.inside.domusdigitalis.dev 'show license all'

ISE 3.3/3.4 includes social login in the base deployment for up to 100 concurrent guest sessions (evaluation mode). Verify your deployment model supports it.

ISE Configuration — Guest Portal + WLAN
Step 1 — Create Guest Portal (Self-Registration + Social Login)

ISE Admin GUI → Work Centers → Guest Access → Portals & Components → Guest Portals:

  1. Clone "Self-Registered Guest Portal" (default template)

  2. Portal name: Domus-Guest-SelfReg

  3. Portal settings:

    • HTTPS port: 8443

    • Certificate: bind the ACME-issued Let’s Encrypt cert (from certmgr-01 automation)

    • Allowed interfaces: GigabitEthernet 0 (or all)

  4. Login page:

    • Enable "Allow social login"

    • Providers: Google, Apple (configure OAuth client ID/secret per provider)

    • Disable username/password self-registration (social login only — reduces spam accounts)

  5. Guest type: Domus_Guest (create if not exists)

    • Access duration: 24 hours (auto-expire)

    • Maximum devices: 3 per guest account

  6. AUP (Acceptable Use Policy): Enable, require acknowledgment before access

Step 2 — Create Sponsor Portal

ISE Admin GUI → Work Centers → Guest Access → Portals & Components → Sponsor Portals:

  1. Clone "Sponsor Portal (default)"

  2. Portal name: Domus-Sponsor

  3. Sponsor groups:

    • ALL_ACCOUNTS — Evan (full sponsor rights: create, suspend, delete)

  4. Guest types available to sponsor: Domus_Guest

  5. Default duration: 8 hours (adjustable by sponsor up to 7 days)

Step 3 — Authorization Profile for Guest VLAN

ISE Admin GUI → Policy → Policy Elements → Results → Authorization → Authorization Profiles:

  1. Profile name: Domus_Guest_Profile

  2. Access type: ACCESS_ACCEPT

  3. VLAN: 40 (Guest VLAN)

  4. DACL: PERMIT_INTERNET_ONLY (create if not exists — permit DNS + HTTP/HTTPS outbound, deny RFC1918)

  5. Web redirection:

    • Type: Centralized Web Auth (CWA)

    • Portal: Domus-Guest-SelfReg

    • ACL: ACL-GUEST-REDIRECT (redirect HTTP/HTTPS to ISE portal)

Step 4 — Policy Set for Guest WLAN

ISE Admin GUI → Policy → Policy Sets:

Create new policy set or add rules to existing:

  1. Policy set name: Domus_Guest_Wireless

  2. Condition: Radius:Called-Station-ID CONTAINS Domus-Guest (matches SSID name in Called-Station-ID)

  3. Authentication:

    • Rule: MAB (MAC Authentication Bypass) — guests don’t have 802.1X supplicants

    • Identity source: Guest_Portal_Sequence

  4. Authorization:

    • Rule 1: If IdentityGroup:Name EQUALS Guest_Domus_GuestDomus_Guest_Profile

    • Rule 2: Default → CWA redirect to Domus-Guest-SelfReg portal

Step 5 — WLC WLAN Profile for Domus-Guest

9800 WLC configuration for the guest SSID:

! Create WLAN
wlan Domus-Guest 3 Domus-Guest
 no security wpa
 security web-auth
 security web-auth authentication-list default
 security web-auth parameter-map global
 no shutdown

! Policy profile
wireless profile policy Domus-Guest-Policy
 vlan {vlan-guest}
 no shutdown

! Policy tag (bind WLAN to policy)
wireless tag policy Domus-Guest-Tag
 wlan Domus-Guest policy Domus-Guest-Policy

! ACL for CWA redirect
ip access-list extended ACL-GUEST-REDIRECT
 permit udp any any eq domain
 permit udp any eq domain any
 permit tcp any host {ise-01-ip} eq 8443
 deny   ip any any
Step 6 — ACME Cert Binding for Portal HTTPS

The self-registration portal needs a trusted HTTPS certificate. Use the ACME-automated Let’s Encrypt cert from certmgr-01:

  1. Cert CN/SAN: guest.inside.domusdigitalis.dev (or wildcard *.inside.domusdigitalis.dev)

  2. Import to ISE via the existing automation script (ise-cert-deploy.sh)

  3. Bind to portal: Portal Settings → Certificate → select the LE-issued cert

  4. Verify: guests should see a valid HTTPS lock icon (no cert warning)

Verification
# Check portal is reachable
curl -sk https://ise-01.inside.domusdigitalis.dev:8443/portal/PortalSetup.action -o /dev/null -w "%{http_code}\n"

# Check guest sessions via MnT
netapi ise mnt active-sessions | jq '.[] | select(.nas_port_id | contains("Domus-Guest"))'

# Check guest accounts via ERS
curl -sk https://ise-01.inside.domusdigitalis.dev:9060/ers/config/guestuser \
  -u "admin:$(gopass show -o v3/domains/d000/identity/ise/ise-01)" \
  -H 'Accept: application/json' | jq '.SearchResult.total'

eap-teap-home-testing

EAP-TEAP Home Testing — Mirror Work GPO Profiles

Why Test EAP-TEAP at Home

The mschapv2-to-cert-migration project (d001) has two GPO-managed WiFi profiles pushing EAP-TEAP (type 55) to Windows endpoints. Wave 3 (Windows) shows 10/10 on GPO deployment but 135 devices still authenticating via MSCHAPv2 — the GPO gap needs investigation.

Testing at home provides:

  • Controlled environment to validate EAP-TEAP supplicant behavior without production risk

  • ISE policy set iteration — test authentication/authorization rules before deploying to work ISE

  • Certificate chain validation — confirm the TEAP outer tunnel accepts the home Vault CA chain

  • Profile comparison — replicate the exact GPO profile parameters and verify ISE handles them identically

  • Work computer testing — the work laptop has two GPO-managed TEAP profiles; connecting it to home ISE validates the profile structure

GPO Profile Parameters to Replicate

From gpo-delta-analysis.adoc (d001 mschapv2-to-cert-migration):

EAP-TEAP profile structure (from work GPO)

EAP-TEAP (type 55) wraps an inner EAP method inside a TLS tunnel:

  • Outer method: TEAP (TLS tunnel establishment)

    • Server cert validation: required (CA chain must be trusted)

    • Anonymous identity: anonymous@inside.domusdigitalis.dev (hides real username in outer tunnel)

  • Inner method: EAP-TLS (certificate-based authentication)

    • Machine cert: auto-enrolled via AD CS / SCEP / Intune

    • User cert: optional (machine-only auth sufficient for network access)

  • Fallback inner method: EAP-MSCHAPv2 (for devices without certs yet — transition period)

The dual inner method is the key: TEAP negotiates EAP-TLS first, falls back to MSCHAPv2 if no client cert is available. This enables phased migration without breaking existing access.

Home Lab Differences
Parameter Work Home (Domus)

CA

AD CS (enterprise root)

Vault PKI (self-signed root)

Cert enrollment

GPO auto-enroll + SCEP

Manual Vault issue + deploy

ISE version

3.3 (production)

3.3 (lab — rotated 2026-06-07)

RADIUS secret

Managed by network team

gopass ise-radius-secret

SSID

Corp WiFi

Domus-Secure (repurpose for TEAP testing)

Inner fallback

MSCHAPv2 (AD credentials)

MSCHAPv2 (FreeIPA credentials)

The home test validates protocol mechanics, not the exact credential store. If TEAP works with Vault certs + FreeIPA fallback, the same ISE policy structure works at work with AD CS certs + AD credentials.

ISE Policy Set — EAP-TEAP Configuration
Step 1 — Allowed Protocols

ISE Admin GUI → Policy → Policy Elements → Results → Authentication → Allowed Protocols:

  1. Clone existing allowed protocols list (or edit Default Network Access)

  2. Name: Domus_TEAP_Protocols

  3. Enable:

    • EAP-TEAP: Yes

    • EAP-TEAP inner methods: EAP-TLS + EAP-MSCHAPv2

    • EAP-TLS: Yes (standalone, for non-TEAP clients)

    • PEAP: No (force TEAP — remove legacy fallback in test environment)

  4. Accept client cert during tunnel establishment: Yes

  5. EAP-TEAP chaining: Allow (machine + user cert in single session)

Step 2 — Certificate Authentication Profile

ISE Admin GUI → Administration → Identity Management → External Identity Sources → Certificate Authentication Profile:

  1. Name: Domus_Cert_Profile

  2. Principal username: Subject - Common Name (CN from client cert)

  3. Certificate attribute: Subject Alternative Name - DNS (fallback)

  4. Binary comparison: Enabled (match cert against ISE internal DB)

Step 3 — Identity Source Sequence

ISE Admin GUI → Administration → Identity Management → Identity Source Sequences:

  1. Name: Domus_TEAP_Sequence

  2. Search list:

    • Domus_Cert_Profile (try cert first)

    • FreeIPA (LDAP fallback for MSCHAPv2 inner method)

    • Internal Users (last resort)

  3. Treat as "access reject" if user not found in any source

Step 4 — Policy Set Rules

Add to existing wired/wireless policy set or create new:

Authentication rules
Rule: TEAP-EAP-TLS
  Condition: Radius:EAP-Type = TEAP
  Allowed Protocols: Domus_TEAP_Protocols
  Identity Source: Domus_TEAP_Sequence

Rule: Standalone-EAP-TLS
  Condition: Radius:EAP-Type = EAP-TLS
  Allowed Protocols: Domus_TEAP_Protocols
  Identity Source: Domus_Cert_Profile
Authorization rules
Rule: TEAP-CertAuth-Admin
  Condition: Certificate:Subject-OU = "Domus-Admins"
             AND Network Access:EapAuthentication = TEAP
  Profile: Domus_Admin_Profile (VLAN 100)

Rule: TEAP-CertAuth-User
  Condition: Network Access:EapAuthentication = TEAP
             AND Network Access:EapTunnel = EAP-TLS
  Profile: Domus_Data_Profile (VLAN 10)

Rule: TEAP-MSCHAPv2-Fallback
  Condition: Network Access:EapAuthentication = TEAP
             AND Network Access:EapTunnel = EAP-MSCHAPv2
  Profile: Domus_Data_Profile (VLAN 10)
  # Fallback: user has no cert yet, authenticated via password
Step 5 — Test from Work Laptop

The work laptop connects to Domus-IoT (iPSK) by default. To test TEAP:

  1. Temporarily create a Domus-TEAP-Test SSID on the 9800 WLC (separate from Domus-Secure)

  2. Profile: WPA2-Enterprise, RADIUS pointing to home ISE

  3. Connect work laptop — it should use the GPO-pushed TEAP profile automatically

  4. Verify in ISE: Live Logs → confirm TEAP outer tunnel + EAP-TLS inner method

  5. Compare ISE Live Log output with work ISE logs (from d001 project) for parity

# Watch live authentications
ssh ise-01.inside.domusdigitalis.dev 'show logging application ise-psc.log tail count 50' | grep -i teap
Validation Criteria
  • TEAP outer tunnel establishes (ISE Live Log shows EAP-Type: TEAP)

  • Inner EAP-TLS succeeds with Vault-issued client cert

  • Inner MSCHAPv2 fallback succeeds with FreeIPA credentials (when cert removed)

  • Correct VLAN assigned based on cert OU or fallback method

  • Work laptop GPO profile connects without manual configuration

  • Session chaining works (machine cert + user cert in single TEAP session)

vyos-firewall-rules

VyOS Firewall Rules — IoT Git + Work Demo Access

Rule 1 — Allow Git from IoT VLAN

IoT devices (VLAN 30) need git access. Current state from show firewall:

  • IOT_WAN rule 20 already allows TCP 80,443 — HTTPS git works

  • IOT_WAN has no rule for TCP 22 — SSH git (git@github.com) is blocked

  • IOT_MGMT has no rule for Gitea — GITEA address group exists (10.50.1.72) but no firewall rule permits access

  • Next available rule numbers: IOT_WAN=95, IOT_MGMT=30

SSH Git Outbound (IOT_WAN)
set firewall ipv4 name IOT_WAN rule 95 action 'accept'
set firewall ipv4 name IOT_WAN rule 95 description 'Allow git SSH outbound'
set firewall ipv4 name IOT_WAN rule 95 destination port '22'
set firewall ipv4 name IOT_WAN rule 95 protocol 'tcp'
Internal Gitea (IOT_MGMT)
set firewall ipv4 name IOT_MGMT rule 30 action 'accept'
set firewall ipv4 name IOT_MGMT rule 30 description 'Allow IoT to Gitea'
set firewall ipv4 name IOT_MGMT rule 30 destination group address-group 'GITEA'
set firewall ipv4 name IOT_MGMT rule 30 destination port '3000,22'
set firewall ipv4 name IOT_MGMT rule 30 protocol 'tcp'

Uses the existing GITEA address group (10.50.1.72) — scoped to a single host, not open to all of MGMT.

Apply via SSH Askpass
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh vyos-01 2>&1 | tee /tmp/vyos-git-rules.log

Then in the VyOS configure session:

configure

show firewall ipv4 name IOT_WAN
show firewall ipv4 name IOT_MGMT

set firewall ipv4 name IOT_WAN rule 95 action 'accept'
set firewall ipv4 name IOT_WAN rule 95 description 'Allow git SSH outbound'
set firewall ipv4 name IOT_WAN rule 95 destination port '22'
set firewall ipv4 name IOT_WAN rule 95 protocol 'tcp'

set firewall ipv4 name IOT_MGMT rule 30 action 'accept'
set firewall ipv4 name IOT_MGMT rule 30 description 'Allow IoT to Gitea'
set firewall ipv4 name IOT_MGMT rule 30 destination group address-group 'GITEA'
set firewall ipv4 name IOT_MGMT rule 30 destination port '3000,22'
set firewall ipv4 name IOT_MGMT rule 30 protocol 'tcp'

compare

commit
save
exit

show firewall ipv4 name IOT_WAN
show firewall ipv4 name IOT_MGMT
Verification
# From an IoT device — test SSH git
git ls-remote git@github.com:evanusmodestus/domus-docs.git 2>&1 | head -3

# From an IoT device — test internal Gitea
git ls-remote ssh://git@10.50.1.72:22/evan/dotfiles.git 2>&1 | head -3
git ls-remote http://10.50.1.72:3000/evan/dotfiles.git 2>&1 | head -3
Security note

IOT_WAN rule 95 opens TCP 22 outbound to any destination. For tighter control, create an address group with specific git server IPs. For a home lab with IoT devices already on a restricted VLAN (no lateral movement to DATA, GUEST, MGMT, or SECURITY zones), the risk is acceptable.

Existing Zone Summary (from VyOS config dump 2026-06-30)
Table 1. IOT_WAN rules (current)
Rule Action Description

10

accept

Established/related

20

accept

HTTP/HTTPS (TCP 80,443)

30

accept

NTP (UDP 123)

40

accept

ICMP outbound

50

accept

DNS (UDP 53)

60

accept

CAPWAP control (UDP 5246)

70

accept

CAPWAP data (UDP 5247)

75

accept

TCP 2443

80

accept

IPsec ESP

85

accept

IPsec NAT-T (UDP 4501)

90

accept

IKE (UDP 500)

95

accept

Git SSH (TCP 22) — NEW

default

drop

Table 2. IOT_MGMT rules (current)
Rule Action Description

10

accept

Established/related

20

accept

Wazuh agent → WAZUH_MANAGER

30

accept

Gitea (TCP 3000,22) → GITEA group — NEW

default

drop (logged)

Table 3. Address groups referenced
Group Members

GITEA

10.50.1.72

WAZUH_MANAGER

10.50.1.134

Rule 2 — Temporary Work Demo Access (IoT → Management)

Work computer stays on Domus-IoT (VLAN 30). Temporary rules grant scoped access to specific management assets for demonstrating the home lab to colleagues. Rules use 50-59 range for easy identification and teardown.

Current IOT_MGMT has rules 10 (established), 20 (Wazuh), 30 (Gitea — if added). Temporary rules start at 50.

Assets to Expose
Demo-accessible services
Service Address Group Port Purpose

ISE Admin GUI

ISE_NODES

443

Policy sets, live logs, endpoint profiling

Vault UI

VAULT_NODES

8200

PKI engine, cert issuance demo

Traefik ingress (Grafana + Wazuh dashboards)

K3S_NODES

443

Dashboard demos via k3s ingress

VyOS Configuration — Temporary Rules
configure

show firewall ipv4 name IOT_MGMT

set firewall ipv4 name IOT_MGMT rule 50 action 'accept'
set firewall ipv4 name IOT_MGMT rule 50 description 'TEMP: Work laptop to ISE GUI'
set firewall ipv4 name IOT_MGMT rule 50 destination group address-group 'ISE_NODES'
set firewall ipv4 name IOT_MGMT rule 50 destination port '443'
set firewall ipv4 name IOT_MGMT rule 50 protocol 'tcp'

set firewall ipv4 name IOT_MGMT rule 51 action 'accept'
set firewall ipv4 name IOT_MGMT rule 51 description 'TEMP: Work laptop to Vault UI'
set firewall ipv4 name IOT_MGMT rule 51 destination group address-group 'VAULT_NODES'
set firewall ipv4 name IOT_MGMT rule 51 destination port '8200'
set firewall ipv4 name IOT_MGMT rule 51 protocol 'tcp'

set firewall ipv4 name IOT_MGMT rule 52 action 'accept'
set firewall ipv4 name IOT_MGMT rule 52 description 'TEMP: Work laptop to k3s ingress'
set firewall ipv4 name IOT_MGMT rule 52 destination group address-group 'K3S_NODES'
set firewall ipv4 name IOT_MGMT rule 52 destination port '443'
set firewall ipv4 name IOT_MGMT rule 52 protocol 'tcp'

compare

commit comment 'TEMP: work demo access — remove after 2026-07-07'
save
exit

show firewall ipv4 name IOT_MGMT
Teardown — Remove Temporary Rules
configure

delete firewall ipv4 name IOT_MGMT rule 50
delete firewall ipv4 name IOT_MGMT rule 51
delete firewall ipv4 name IOT_MGMT rule 52

compare

commit comment 'Remove TEMP work demo rules'
save
exit

show firewall ipv4 name IOT_MGMT
Track these rules

Temporary firewall rules are technical debt. The commit comment embeds the expiry date in VyOS commit history. Also added to appendix-todos.adoc P1.

Never let "temporary" become permanent. The IoT VLAN exists specifically to isolate untrusted devices from management infrastructure.

VyOS CLI Patterns — Process Sub + Command Sub
Process Substitution — Diff Without Temp Files
Diff two firewall zones side by side
ssh vyos-01 'diff <(show firewall ipv4 name IOT_MGMT) <(show firewall ipv4 name IOT_WAN)'
Diff active config vs saved config (running vs startup)
ssh vyos-01 'diff <(show config) <(cat /config/config.boot)'

Only quoting differences = configs match. Real changes show as +/- lines.

Diff firewall before and after a change (from workstation)
# Snapshot before
ssh vyos-01 'show firewall ipv4 name IOT_MGMT' > /tmp/iot-mgmt-before.txt

# Make changes on VyOS...

# Diff against live
diff /tmp/iot-mgmt-before.txt <(ssh vyos-01 'show firewall ipv4 name IOT_MGMT')
Compare rulesets between vyos-01 and vyos-02 (HA pair)
diff <(ssh vyos-01 'show config commands | sort') <(ssh vyos-02 'show config commands | sort')

Zero diff = HA pair is in sync. Any output = config drift.

Compare address group membership
ssh vyos-01 'diff <(show firewall group address-group ADMINS) <(show firewall group address-group ISE_NODES)'
Command Substitution — Embed Output as Arguments
Count firewall rules per zone
ssh vyos-01 'show config commands' | grep -c 'IOT_WAN rule'
ssh vyos-01 'show config commands' | grep -c 'IOT_MGMT rule'
Extract all zone names
ssh vyos-01 'show config commands' | grep -oP 'name \K[A-Z_]+' | sort -u
Find which zones reference an address group
ssh vyos-01 'show config commands' | grep 'GITEA'
ssh vyos-01 'show config commands' | grep 'ISE_NODES'
Open the firewall config for a specific zone in nvim (read-only inspection)
nvim <(ssh vyos-01 'show firewall ipv4 name IOT_WAN')
Diff two zones in nvim split view
nvim -d <(ssh vyos-01 'show firewall ipv4 name IOT_WAN') \
        <(ssh vyos-01 'show firewall ipv4 name DATA_WAN')
Operational Patterns
Show all rules that reference a specific port
ssh vyos-01 'show config commands' | grep "port '22'"
ssh vyos-01 'show config commands' | grep "port '443'"
Show all drop counters (which zones are blocking traffic)
ssh vyos-01 'show firewall' | grep -B2 'default  drop' | grep -v '^--$'
Show zones with logged drops (troubleshooting)
ssh vyos-01 'show config commands' | grep 'default-log'
Show commit history (VyOS tracks config changes)
ssh vyos-01 'show system commit'
Rollback to previous config (careful — reverses last commit)
configure
rollback 1
compare
commit
save
Cisco → VyOS Translation
Cisco IOS VyOS Notes

show running-config

show config or show configuration

Active config

show startup-config

cat /config/config.boot

Saved to disk

show run | section firewall

show config commands | grep firewall

Filter by section

copy run start

save (in configure mode)

Persist to disk

reload

reboot or reboot now

Full restart

show ip access-lists

show firewall

All zone rulesets with counters

show ip int brief

show interfaces

Interface summary

show vrrp

show vrrp

Same command

write erase

load /opt/vyatta/etc/config.boot.default

Factory reset (careful)

android-vault-cert

Android Vault Cert — Z Fold 7 Provisioning

gopass v4 — Vault Cluster Entry

Migrated Vault init credentials from flat env file to gopass v4 YubiKey-encrypted structured YAML entry.

Entry Path

v4/domains/d000/identity/vault/cluster

Structure

Follows the gopass v4 YAML standard — typed metadata, nested credential groups with _use annotations, network topology, PKI configuration, and service inventory. All string values quoted. Matches the pattern established by v4/domains/d000/network/devices/firewall/vyos-01.

Top-Level Keys
type, hostname, ip, model, version, role, deployment,
tier, location, credentials, network, pki, services,
created, updated
Credential Groups
Path Purpose

.credentials.unseal.key-{1,2,3}

Shamir shares — 2 of 3 required to unseal

.credentials.root-token.token

Emergency break-glass token

.credentials.operational.token

Day-to-day API access (sourced via ds d000 dev/vault)

.credentials.api.addr

Active node HTTPS endpoint

.credentials.api.cacert

CA cert path for TLS verification

PKI Fields
Path Purpose

.pki.role

Cert issuance role (domus-client)

.pki.root-path / .pki.int-path

Secrets engine mount paths (pki, pki_int)

.pki.ssh-signer

SSH CA engine (ssh-client-signer)

.pki.allowed-domains

Permitted cert domain

Design Decisions
  • Structured YAML with nested credentials — matches vyos-01 entry pattern. _use annotations make each credential group self-documenting.

  • All values quoted — proper YAML. Prevents type coercion. yq -r strips quotes on extraction.

  • One entry per cluster — one YubiKey touch for any operational task.

  • No serials/fingerprints — derived values: vault read -field=certificate pki/cert/ca | openssl x509 -noout -serial -fingerprint

Usage Patterns
Unseal a sealed node
gopass show v4/domains/d000/identity/vault/cluster \
  | yq -r '.credentials.unseal."key-1"' \
  | VAULT_ADDR="https://<node>.inside.domusdigitalis.dev:8200" vault operator unseal -
gopass show v4/domains/d000/identity/vault/cluster \
  | yq -r '.credentials.unseal."key-2"' \
  | VAULT_ADDR="https://<node>.inside.domusdigitalis.dev:8200" vault operator unseal -
Retrieve operational token
gopass show v4/domains/d000/identity/vault/cluster | yq -r '.credentials.operational.token'
Query PKI role
gopass show v4/domains/d000/identity/vault/cluster | yq -r '.pki.role'
List cluster nodes
gopass show v4/domains/d000/identity/vault/cluster \
  | yq -r '.network.nodes | to_entries[] | "\(.key)  \(.value)"'
Verify structure (never values)
gopass show v4/domains/d000/identity/vault/cluster | yq 'keys'
gopass show v4/domains/d000/identity/vault/cluster | yq '.credentials | keys'
Cert Provisioning — Updated Vault Cert to Z Fold 7

The Vault PKI intermediate CA cert was rotated during the ISE rotation (2026-06-07). The Z Fold 7 needs the updated cert chain to continue connecting to Domus-Secure (EAP-TLS) and SSH to lab hosts (Vault-signed SSH certs).

What Needs Updating
Cert Current State Action

Vault Root CA

May still have old root if Vault PKI was re-initialized

Verify → install if changed

Vault Intermediate CA

Rotated 2026-06-07 during ISE rotation

Install updated intermediate

Client EAP-TLS cert

Issued by old intermediate — may be invalid

Re-issue from new intermediate

SSH signed cert

30-day validity — check expiry

Re-sign if expired

Step 0 — Source Vault Credentials + Verify Cluster Health
ds d000 dev/vault

Check seal state and HA role across all three nodes via API — no SSH needed (FIDO2 keys make SSH loops impractical). /v1/sys/health is unauthenticated — no token required:

for node in vault-01 vault-02 vault-03; do
  printf "%-12s " "$node"
  curl -sk "https://$node.inside.domusdigitalis.dev:8200/v1/sys/health" \
    | jq -r '"\(if .sealed then "SEALED" else "unsealed" end)  \(if .standby then "standby" else "ACTIVE" end)  v\(.version)"' \
    2>/dev/null || echo "UNREACHABLE"
done

Three outcomes per node: unsealed ACTIVE (leader), unsealed standby (follower), or SEALED / UNREACHABLE.

If any node shows SEALED, unseal it — two rounds per node (Shamir threshold 2 of 3). Extract keys from gopass v4 to clipboard, paste into the interactive prompt:

# Copy key-1 to clipboard
# sed strips gopass stdout pollution (YubiKey OTP, blank lines) before the YAML
gopass show v4/domains/d000/identity/vault/cluster 2>/dev/null \
  | sed -n '/^type:/,$ p' | yq -r '.credentials.unseal."key-1"' | wl-copy

# Unseal — paste when prompted
vault operator unseal -address="https://<node>.inside.domusdigitalis.dev:8200"

# Clear clipboard immediately
wl-copy ''

# Repeat with key-2
gopass show v4/domains/d000/identity/vault/cluster 2>/dev/null \
  | sed -n '/^type:/,$ p' | yq -r '.credentials.unseal."key-2"' | wl-copy
vault operator unseal -address="https://<node>.inside.domusdigitalis.dev:8200"
wl-copy ''
Piping directly to vault operator unseal - (stdin mode) fails with "key must be valid hex or base64" despite the key being valid. The clipboard workflow via wl-copy is the reliable path.

Re-run the health check to confirm all three nodes are unsealed. Check version drift at the same time — all nodes should run the same Vault version:

for node in vault-01 vault-02 vault-03; do
  printf "%-12s " "$node"
  curl -sk "https://$node.inside.domusdigitalis.dev:8200/v1/sys/health" \
    | jq -r '"\(if .sealed then "SEALED" else "unsealed" end)  \(if .standby then "standby" else "ACTIVE" end)  v\(.version)"' \
    2>/dev/null || echo "UNREACHABLE"
done

As of 2026-06-30: vault-01 is v1.21.2, vault-02/03 are v1.21.4. Upgrade vault-01 to match (tracked in appendix-todos P1).

Verify your token works against the active node:

vault status | grep -E 'Sealed|HA Mode|Active Node'
vault token lookup | awk '/display_name|expire_time|policies/'

awk extracts the three fields that matter: identity, expiry, and permissions. If expire_time is past, re-source: ds d000 dev/vault.

Step 1 — Confirm PKI Engines + Role
vault secrets list -format=json \
  | jq -r 'to_entries[] | select(.key | startswith("pki") or startswith("ssh")) | "\(.key)  \(.value.type)  \(.value.description)"'
vault read -format=json pki_int/roles/domus-client \
  | jq '.data | {allowed_domains, organization, ou, key_type, key_bits, max_ttl}'

Verify organization contains Domus-Infrastructure and ou contains Domus-Admins — ISE authorization matches on Certificate.Subject-OU.

Step 2 — Export CA Chain and Verify
vault read -field=certificate pki/cert/ca > /tmp/vault-root-ca.pem
vault read -field=certificate pki_int/cert/ca > /tmp/vault-intermediate-ca.pem

openssl x509 -in /tmp/vault-root-ca.pem -noout -subject -dates
openssl x509 -in /tmp/vault-intermediate-ca.pem -noout -subject -issuer -dates

openssl verify -CAfile /tmp/vault-root-ca.pem /tmp/vault-intermediate-ca.pem

The openssl x509 commands confirm you’re looking at the right certs before issuing anything against them. If the root or intermediate have changed since the phone was last provisioned, the phone needs both reinstalled.

Step 3 — Issue New Client Cert for Z Fold 7

Verify the role allows the name pattern before issuing:

vault read -format=json pki_int/roles/domus-client \
  | jq '.data | {allowed_domains, allow_subdomains, allow_bare_domains, allow_glob_domains, allow_any_name}'

allow_bare_domains: false means only FQDNs under inside.domusdigitalis.dev are permitted — no bare hostnames in CN or SANs.

vault write -format=json pki_int/issue/domus-client \
  common_name="zfold7.inside.domusdigitalis.dev" \
  ttl="8760h" > /tmp/zfold7-cert.json

Extract and verify:

jq -r '.data.certificate' /tmp/zfold7-cert.json > /tmp/zfold7.pem
jq -r '.data.private_key' /tmp/zfold7-cert.json > /tmp/zfold7-key.pem
jq -r '.data.ca_chain[]' /tmp/zfold7-cert.json > /tmp/zfold7-ca-chain.pem

openssl x509 -in /tmp/zfold7.pem -noout -subject -issuer -dates
openssl verify -CAfile /tmp/vault-root-ca.pem -untrusted /tmp/vault-intermediate-ca.pem /tmp/zfold7.pem

Confirm the subject shows CN=zfold7.inside.domusdigitalis.dev and the chain validates cleanly. If the role includes organization and ou, verify those appear in the subject too.

Step 4 — Bundle as PKCS12 for Android
openssl pkcs12 -export \
  -in /tmp/zfold7.pem \
  -inkey /tmp/zfold7-key.pem \
  -certfile /tmp/zfold7-ca-chain.pem \
  -out /tmp/zfold7.p12 \
  -name "Domus EAP-TLS" \
  -passout pass:changeit

To set a custom password instead of changeit, omit -passout and enter interactively:

openssl pkcs12 -export \
  -in /tmp/zfold7.pem \
  -inkey /tmp/zfold7-key.pem \
  -certfile /tmp/zfold7-ca-chain.pem \
  -out /tmp/zfold7.p12 \
  -name "Domus EAP-TLS"

Verify the bundle:

openssl pkcs12 -in /tmp/zfold7.p12 -info -nokeys -passin pass:changeit 2>/dev/null \
  | grep -E 'subject|issuer'
Step 5 — Transfer to Phone

The phone needs the root CA and intermediate CA as separate PEM files, plus the PKCS12 bundle.

# Option A: via Termux SSH
scp /tmp/vault-root-ca.pem /tmp/vault-intermediate-ca.pem /tmp/zfold7.p12 \
  zfold7:/sdcard/Download/

# Option B: via USB
adb push /tmp/vault-root-ca.pem /sdcard/Download/
adb push /tmp/vault-intermediate-ca.pem /sdcard/Download/
adb push /tmp/zfold7.p12 /sdcard/Download/

# Option C: via NAS
cp /tmp/vault-root-ca.pem /tmp/vault-intermediate-ca.pem /tmp/zfold7.p12 \
  /mnt/nas/transfer/
Step 6 — Install on Android

Android presents three certificate stores. Use the correct one for each cert type:

Store (Spanish) Use

Certificado de CA

Root CA + Intermediate CA (trust chain)

Certificado de Wi-Fi

Client PKCS12 for EAP-TLS (zfold7.p12)

Cert. de usuario de aplic. y VPN

NOT this one — VPN/app-level TLS only, WiFi supplicant does not read from here

Install CA certificates first (order matters — root before intermediate):

  1. Settings → Security → More security settings → Encryption & credentials

  2. Install a certificate → Certificado de CA → select vault-root-ca.pem

  3. Confirm trust warning → Name: "Domus Root CA"

  4. Repeat for vault-intermediate-ca.pem → Name: "Domus Intermediate CA"

Install client certificate:

  1. Install a certificate → Certificado de Wi-Fi → select zfold7.p12

  2. Enter PKCS12 password (changeit)

  3. Name: "Domus EAP-TLS"

Step 7 — Disable Randomized MAC + Configure WiFi Profile

Samsung uses randomized MACs by default. ISE needs a consistent MAC for endpoint tracking. Fix this before connecting.

Disable MAC randomization:

  1. Settings → WiFi → Domus-Secure (gear icon or long press)

  2. Advanced → Tipo de dirección MAC → MAC del teléfono (not "MAC aleatoria")

  3. Save

Verify via adb (after connecting):

adb -s R3GYB0J7YHY shell ip addr show wlan0 | awk '/link\/ether/{print $2}'

The real MAC starts with a Samsung OUI (e.g. 00:, 04:, 14:, 1C:, 20:). If you still see 66:xx (locally administered bit), randomization is still on.

WiFi profile settings (Spanish labels → exact values):

Campo (Field) Valor (Value)

Red (Network)

Domus-Secure

Seguridad (Security)

WPA2/WPA3-Enterprise

Método EAP (EAP method)

TLS

Certificado CA (CA certificate)

Domus Root CA

Certificado de usuario (User certificate)

Domus EAP-TLS

Identidad (Identity)

anonymous@inside.domusdigitalis.dev

Identidad anónima (Anonymous identity)

anonymous@inside.domusdigitalis.dev

Tipo de dirección MAC (MAC address type)

MAC del teléfono (Phone MAC)

Dominio (Domain)

inside.domusdigitalis.dev

Both identity fields set to anonymous@inside.domusdigitalis.dev. The real identity is derived from the certificate CN (zfold7.inside.domusdigitalis.dev) inside the encrypted EAP-TLS tunnel. The outer identity is sent in cleartext — setting it to anonymous prevents exposing the real username over the air. Same pattern as the mschapv2-to-cert-migration project.

Save MAC for ISE queries — after the phone connects with its real MAC:

ZFOLD7_MAC=$(adb -s R3GYB0J7YHY shell ip addr show wlan0 | awk '/link\/ether/{print $2}' | tr '[:lower:]' '[:upper:]')
echo "Z Fold 7 WiFi MAC: ${ZFOLD7_MAC}"
Step 8 — Verify from Workstation

ISE API reference: data/d000/infra/ise-api-reference/

# Load ISE credentials
dsource d000 dev/network/ise

# Source the API helpers
source data/d000/infra/ise-api-reference/scripts/lib/ise-common.sh
# MnT — active session by MAC (returns XML → xq)
MAC="E0:D5:5D:6C:E1:66"
mnt "/Session/MACAddress/${MAC}" | xq -C '.'
# DataConnect — auth history (validated columns from endpoint-check.adoc)
dc_query "
  SELECT calling_station_id AS mac, username, identity_group,
         authentication_method, authentication_protocol,
         policy_set_name, authorization_rule, authorization_profiles,
         passed, failed, failure_reason,
         nas_ip_address, device_name, timestamp_timezone
  FROM radius_authentications
  WHERE calling_station_id = '${MAC}'
  ORDER BY timestamp_timezone DESC
  FETCH FIRST 5 ROWS ONLY
" | jq -C '.' | tee /tmp/zfold7-auth-check.json
# Full endpoint profile — 5-table JOIN + cert/TLS + 3-API
full-endpoint-profile.sh E0:D5:5D:6C:E1:66

ISE Live Logs should show:

  • Authentication method: EAP-TLS

  • Subject CN: zfold7.inside.domusdigitalis.dev

  • VLAN assigned: 10 (Data)

  • Result: PASS

Common failure reasons:

  • "Certificate chain incomplete" → root CA not trusted on ISE (import via Admin → Certificates → Trusted Certificates)

  • "Certificate has expired" → Vault intermediate was not actually rotated, or TTL issue

  • "Subject mismatch" → CN in cert doesn’t match ISE identity source lookup

Step 9 — Re-sign SSH Cert (if expired)
ds d000 dev/vault

vault write -field=signed_key ssh-client-signer/sign/domus-user \
  public_key=@~/.ssh/zfold7_ed25519.pub \
  valid_principals="evan" \
  cert_type=user > /tmp/zfold7-ssh-cert.pub

ssh-keygen -L -f /tmp/zfold7-ssh-cert.pub

scp /tmp/zfold7-ssh-cert.pub zfold7:~/.ssh/zfold7_ed25519-cert.pub
Cleanup
rm -f /tmp/zfold7-cert.json /tmp/zfold7.pem /tmp/zfold7-key.pem \
      /tmp/zfold7-ca-chain.pem /tmp/zfold7.p12 /tmp/zfold7-ssh-cert.pub \
      /tmp/vault-root-ca.pem /tmp/vault-intermediate-ca.pem

Delete from phone after installation:

rm -f /sdcard/Download/vault-root-ca.pem \
      /sdcard/Download/vault-intermediate-ca.pem \
      /sdcard/Download/zfold7.p12
Private key hygiene

The PKCS12 file contains the private key. After installing on the phone:

  • Delete from /sdcard/Download/ immediately

  • Delete from NAS/transfer share if used

  • Delete from /tmp/ on workstation

  • The private key now lives only in Android Keystore (hardware-backed on Samsung Knox)

EAP-TLS Debug — Android / ISE / WLC

Troubleshooting workflow for EAP-TLS failures between Android device and ISE via 9800 WLC.

Phone Details
Field Value

Device

Samsung Galaxy Z Fold 7 (SM-F966U1)

Hardware WiFi MAC

9C:83:06:CE:89:46

ADB serial

R3GYB0J7YHY

AP BSSID

a0:a4:7f:20:dd:0e (C9130AX-01)

SSID

Domus-Secure (WLAN 4)

Cert CN (new)

zfold7.inside.domusdigitalis.dev

Cert CN (old — expired)

zfold7-evanusmodestus.byod.inside.domusdigitalis.dev

WLC WLAN

4 (Domus-Secure)

WLC client state

Run / Dot1x / 11ax(5) / Local

Layer 1 — Android Supplicant (adb)
WiFi state machine — shows association, EAP method, TLS result
adb -s R3GYB0J7YHY shell dumpsys wifi \
  | grep -E 'EAP_METHOD|OPEN_SSL|FOUR_WAY|COMPLETED|FAIL|CONNECT|DISCONNECT|L2Connect|AUTHENTICAT' \
  | tail -20
WiFi interface state (UP/DOWN, current MAC)
adb -s R3GYB0J7YHY shell ip addr show wlan0 | awk '/state|link\/ether/{print}'
BSSID blocklist history
adb -s R3GYB0J7YHY shell dumpsys wifi \
  | grep -E 'Bssid (un)?blocked.*Domus-Secure' | tail -10
Installed certificates
adb -s R3GYB0J7YHY shell dumpsys wifi | grep -i 'certificate\|cert\|CA' | head -10

Success indicators: FOUR_WAY_HANDSHAKECOMPLETED. Failure: OPEN_SSL_FAILUREAUTHENTICATION_FAILURE_EVENT.

Layer 2 — WLC (SSH to 9800-wlc-01)
Client summary — is phone associated?
show wireless client summary
Client detail by MAC
show wireless client mac-address 9c83.06ce.8946 detail
WLAN config — verify RADIUS is configured
show wlan name Domus-Secure
RADIUS server status
show radius statistics
Real-time EAP debug (120 seconds)
debug wireless mac 9c83.06ce.8946 monitor-time 120
Stop debug
no debug wireless mac 9c83.06ce.8946
Layer 3 — ISE CLI via Askpass (no interactive password)

ISE uses keyboard-interactive auth — requires SSH_ASKPASS with setsid -w. Password extracted from gopass v4 via the gopass-ssh-askpass.sh script. See gopass-v4-standardization/ise-ssh-pattern.adoc for full documentation.

Shorthand for all ISE CLI commands below
ise_cmd() {
  SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
    SSH_ASKPASS_REQUIRE=force setsid -w ssh ise-01 "$1" \
    2>&1 | tee "/tmp/ise-${2:-output}.txt"
}
Check ISE health — are services running?
ise_cmd 'show application status ise' app-status
Check memory — low RAM degrades MnT, live logs, and DataConnect
ise_cmd 'show process memory' memory
RADIUS/EAP processing log — filter by phone MAC
ise_cmd 'show logging application prrt-server.log tail count 500 | include 9c83' prrt-9c83
ise_cmd 'show logging application prrt-server.log tail count 500 | include 12521' prrt-12521
Enable EAP debug (WARNING: increases log volume — disable after)
ise_cmd 'debug-config runtime-AAA debug-level DEBUG component-name runtime.eap' debug-on
Check debug output after triggering phone connection
ise_cmd 'show logging application prrt-server.log tail count 100 | include EAP' eap-debug
Disable EAP debug
ise_cmd 'debug-config runtime-AAA debug-level WARN component-name runtime.eap' debug-off
Cert health warnings
ise_cmd 'show logging application prrt-server.log tail count 50 | include Crypto' crypto
ISE system certificates
ise_cmd 'show certificate application-server' certs

ISE CLI pipe supports: begin, include, exclude, count, linnum, nomore, save, until. No grep.

Layer 4 — ISE API (from workstation)
Load credentials + helpers
dsource d000 dev/network/ise
source data/d000/infra/ise-api-reference/scripts/lib/ise-common.sh
List rejected endpoints (ERS)
curl -sS --cacert "${ISE_CA_CERT}" -u "${ISE_API_USER}:${ISE_API_PASS}" \
  -H "Accept: application/json" \
  "https://${ISE_PAN_FQDN}:${ISE_ERS_PORT}/ers/config/rejectedendpoint" \
  | jq -r '["MAC","ID"], (.SearchResult.resources[] | [.name, .id]) | @tsv' \
  | column -ts $'\t'
Release rejected endpoint
MAC="9C:83:06:CE:89:46"
EID=$(curl -sS --cacert "${ISE_CA_CERT}" -u "${ISE_API_USER}:${ISE_API_PASS}" \
  -H "Accept: application/json" \
  "https://${ISE_PAN_FQDN}:${ISE_ERS_PORT}/ers/config/rejectedendpoint" \
  | jq -r ".SearchResult.resources[] | select(.name == \"${MAC}\") | .id")
curl -sS --cacert "${ISE_CA_CERT}" -u "${ISE_API_USER}:${ISE_API_PASS}" \
  -H "Accept: application/json" -X DELETE \
  "https://${ISE_PAN_FQDN}:${ISE_ERS_PORT}/ers/config/rejectedendpoint/$\{EID}" | jq '.'
Failed auths — last hour (DataConnect, validated columns)
dc_query "
SELECT TIMESTAMP_TIMEZONE, USERNAME, CALLING_STATION_ID,
       NAS_IP_ADDRESS, AUTHENTICATION_METHOD, FAILURE_REASON,
       POLICY_SET_NAME
FROM radius_authentications
WHERE failure_reason IS NOT NULL
  AND TIMESTAMP_TIMEZONE > SYSTIMESTAMP - INTERVAL '1' HOUR
ORDER BY TIMESTAMP_TIMEZONE DESC
FETCH FIRST 20 ROWS ONLY
" | jq -C '.'
Auth history for specific MAC (DataConnect)
MAC="9C:83:06:CE:89:46"
dc_query "
SELECT calling_station_id AS mac, username, identity_group,
       authentication_method, authentication_protocol,
       policy_set_name, authorization_rule, authorization_profiles,
       passed, failed, failure_reason,
       nas_ip_address, device_name, timestamp_timezone
FROM radius_authentications
WHERE calling_station_id = '${MAC}'
ORDER BY timestamp_timezone DESC
FETCH FIRST 5 ROWS ONLY
" | jq -C '.'
Verification — Capture to Files

After successful connection, capture evidence:

# EAP handshake result
adb -s R3GYB0J7YHY shell dumpsys wifi \
  | grep -E 'EAP_METHOD|OPEN_SSL|FOUR_WAY|COMPLETED|state:.*Domus' \
  | tail -10 | tee /tmp/zfold7-eap-result.txt

# Connection details — MAC, IP, SSID, speed, standard
adb -s R3GYB0J7YHY shell dumpsys wifi \
  | grep 'mWifiInfo' | tee /tmp/zfold7-wifi-info.txt

# IP address assigned
adb -s R3GYB0J7YHY shell ip addr show wlan0 \
  | awk '/inet /{print $2}' | tee /tmp/zfold7-ip.txt
# ISE auth verification (DataConnect — validated columns)
MAC="9C:83:06:CE:89:46"
dc_query "
SELECT calling_station_id AS mac, username, identity_group,
       authentication_method, authentication_protocol,
       policy_set_name, authorization_rule, authorization_profiles,
       passed, failed, failure_reason,
       nas_ip_address, device_name, timestamp_timezone
FROM radius_authentications
WHERE calling_station_id = '${MAC}'
ORDER BY timestamp_timezone DESC
FETCH FIRST 5 ROWS ONLY
" | jq -C '.' | tee /tmp/zfold7-ise-auth.json
Result — 2026-06-30 20:37 PDT
Field Value

Status

COMPLETED — EAP-TLS handshake succeeded

MAC

9C:83:06:CE:89:46 (hardware — not randomized)

IP

10.50.10.145/24 (VLAN 10 — Data)

SSID

Domus-Secure

Standard

802.11ax (WiFi 6)

Frequency

5620 MHz (5 GHz)

RSSI

-29 dBm (excellent)

Link speed

573 Mbps TX/RX

AP BSSID

a0:a4:7f:20:dd:0e (C9130AX-01)

Cert CN

zfold7.inside.domusdigitalis.dev (new cert)

Known Issues (2026-06-30)
Issue Detail

ISE identity store mismatch

Phone MAC 9C:83:06:CE:89:46 previously onboarded via iPSK (Domus-IoT). ISE routes through iPSKManager identity store instead of cert profile on Dot1x attempt.

Android CA cert not visible in WiFi profile

User-installed CA certs require MDM to appear in WiFi cert picker. Workaround: set CA certificate to "Don’t verify" for testing.

Randomized MAC

Samsung default. Must set MAC address type to "Phone MAC" per SSID. Hardware MAC: 9C:83:06:CE:89:46.

Endpoint rejected loop

Repeated EAP failures trigger ISE automatic rejection. Must release via ERS API before retrying.

ISE error 12521

EAP-TLS failed SSL/TLS handshake after a client alert — phone sends TLS fatal alert to ISE. Root cause: cert chain or identity store mismatch, not network connectivity.

ISE low RAM

Low memory degrades MnT service, DataConnect queries return empty, GUI live logs stop populating. Check with show process memory and show application status ise.

ISE default log level

INFO/WARN only — no per-session EAP detail. Enable with debug-config runtime-AAA debug-level DEBUG component-name runtime.eap. Disable after troubleshooting.

MnT API empty

ActiveCount returns 200 but empty body. MnT not tracking sessions. Services show running but session data not being collected. Investigate MnT node role configuration.

DataConnect delay

Successful auth at 20:37 not in DB 30 minutes later. Processing delay longer than expected. Use WLC show wireless client summary for real-time proof instead.

ers_all zsh incompatible

trap RETURN not supported in zsh. Use ers "/endpoint?size=100" directly instead of ers_all.

ers nested in $() triggers zsh math error

UUIDs in $(ers "/endpoint/${eid}" | jq …​) cause zsh: bad math expression. Fix: cache response in variable first, extract with jq <<< "$var". See endpoints.adoc line 153-178 for the validated pattern.

ISE restart 20:54 PDT

Live logs and MnT not showing any clients. Restart initiated to clear potential ISE 3.5 session tracking bug. All 802.1X sessions will drop and reconnect.

domus-infra-updates

Domus Infrastructure — Updates

Wireless Partial Updates

The wireless.adoc partial in domus-infrastructure needs two new sections:

  1. Domus-Guest SSID — WLAN profile for guest portal (sponsor + social login)

  2. EAP-TEAP testing — new allowed protocols and policy set for TEAP validation

These are documented in separate daily note concerns (guest-portal-ise and eap-teap-home-testing). Once validated, the configurations graduate into the domus-infrastructure project partials.

Pending Infrastructure Items (Carryover)

From June 28:

  • SSH 3560CX legacy crypto — 00-exceptions.conf patch ready, not applied

  • SSH modestus-p50 — needs IP for 80-d000-hosts.conf

  • gopass YAML for switch — template at /tmp/gopass-3560cx-01.yaml ready for gopass edit

Build and View
build-adoc "$(find data/d000/projects/domus-infrastructure -maxdepth 1 -name 'domus-infrastructure.adoc' -print -quit)" html --variant light-cyan -a env-d000
firefox "$(find data/d000/projects/domus-infrastructure/output -name '*.html' -print -quit)" &

gopass-v4-standardization

gopass v4 Standardization

Audit all v4 entries for nested credentials structure, _use annotations, quoted values, and yq extractability. Automate SSH authentication via SSH_ASKPASS and deploy Vault SSH CA trust to VyOS firewalls.

Overview
What This Solves

SSH authentication to lab infrastructure requires passwords and passphrases stored in gopass. The v4 entries were flat YAML — all fields at the top level — making programmatic extraction fragile and inconsistent across entry types.

This work standardizes all entries to a nested credentials structure, builds reliable yq extraction tooling, and automates SSH authentication via SSH_ASKPASS.

The Standard — Nested credentials

Every gopass v4 entry follows this structure:

type: "entry-type"
hostname: "device.inside.domusdigitalis.dev"
ip: "10.50.1.x"
domain: "d000"
purpose: "What this device does"
credentials:                          (1)
  ssh:                                (2)
    _use: "Human-readable explanation"
    username: "admin"
    password: "value"
  api:                                (3)
    _use: "API access description"
    key: "value"
    port: "8443"
created: "YYYY-MM-DD"
updated: "YYYY-MM-DD"
1 All secrets nest under credentials — never at the top level
2 Grouped by function: ssh, api, cli, web, console, enable, unseal, etc.
3 Multiple credential groups per entry — one device may have SSH, API, and SNMP credentials

Why nested: gps device '.credentials | keys' shows what access methods exist without exposing values. gps device '.credentials.ssh.password' | wl-copy extracts a specific secret. Flat entries required knowing every field name in advance.

The gps Function — Reliable gopass + yq

Direct piping (gopass show | yq) fails randomly because YubiKey serial negotiation prompts bleed into stdout, corrupting the YAML before yq reads it.

gps() {
  local tmp=$(mktemp)
  gopass show "$1" >| "$tmp" && {
    awk '/^[a-z].*: /{p=1} p' "$tmp" | yq "${@:2}"
  }
  rm -f "$tmp"
}

How it works:

  1. mktemp — temp file isolates decryption from parsing

  2. gopass show >| "$tmp">| overrides zsh noclobber; YubiKey prompts go to terminal, YAML goes to file

  3. awk '/^[a-z].*: /{p=1} p' — state machine gate: starts printing at the first YAML key (type: …​), drops all YubiKey prompt lines before it

  4. yq "${@:2}" — all arguments after the gopass path become the yq expression

  5. rm -f "$tmp" — cleanup runs regardless of success

Usage patterns — safe (no values exposed):

gps v4/domains/d000/identity/ssh/github 'keys'                           # top-level keys
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | keys'        # credential fields
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | length'      # field count
gps v4/domains/d000/identity/ssh/github '{type, service, domain}'        # metadata subset
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | del(.passphrase)'  # all except secret

Usage patterns — extraction to clipboard:

gps v4/domains/d000/identity/ssh/github -r '.credentials.ssh.passphrase' | wl-copy
gps v4/domains/d000/identity/ise/ise-01 -r '.credentials.cli.password' | wl-copy
gps v4/domains/d000/network/switch/3560cx-01 -r '.credentials.ssh.password' | wl-copy
SSH Automation — gopass-ssh-askpass

The gopass-ssh-askpass.sh script serves as SSH_ASKPASS — SSH calls it instead of prompting interactively. It parses the prompt to determine which gopass entry to extract from.

Two prompt types handled:

  1. Key passphrase"Enter passphrase for …​/id_ed25519_github:" → extracts .credentials.ssh.passphrase

  2. Device password"admin@10.50.1.10 Password:" → maps hostname/IP to gopass entry → extracts .credentials.ssh.password

Loading all SSH keys at once:

export SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh
export SSH_ASKPASS_REQUIRE=force
for key in github gitlab gitea vault; do
  ssh-add ~/.ssh/id_ed25519_"$key"
done
Device SSH Patterns

Three authentication patterns exist across the infrastructure:

Pattern Devices Auth Method SSH Command

Vault cert

kvm-01, kvm-02, vyos-01, vyos-02

Vault-signed SSH certificate

ssh vyos-01 "command"

Askpass password + setsid

3560cx-01, 9800-wlc-01, 9800-wlc-02, ise-01

keyboard-interactive via gopass

SSH_ASKPASS=…​ setsid -w ssh 3560cx-01 "command"

Vault cert (daily renewal)

vault-01, vault-02, vault-03

Vault-signed cert (8h TTL)

ds d000 dev/vault && vault-ssh-sign

Why setsid -w for Cisco devices: Cisco uses keyboard-interactive authentication, not password. SSH only calls SSH_ASKPASS when there’s no controlling terminal. setsid -w detaches the terminal, forcing SSH to use askpass. The -w flag waits for the child to exit.

VyOS remote commands require the operational mode wrapper:

ssh vyos-01 "/opt/vyatta/bin/vyatta-op-cmd-wrapper show version"
WSL Arch Portability

Same paths, same git trees, same tools. wl-copy works via WSLg (Wayland).

YubiKey USB passthrough — required for gopass decryption in WSL:

# PowerShell (Admin) — list USB devices
usbipd list

# Attach YubiKey to WSL (run each time after insert)
usbipd bind --busid <BUSID>
usbipd attach --wsl --busid <BUSID>
# WSL — verify YubiKey is visible
ykman info

# If pcscd is needed (smart card daemon)
sudo systemctl start pcscd

Once the YubiKey is attached, gps, gopass sync, ssh-add via askpass, and all extraction commands work identically.

First-time setup on WSL:

  1. Install: gopass, age, yq, ykman, pcscd

  2. Clone gopass stores: gopass clone or gopass sync

  3. Add gps() function to .zshrc (stowed from dots-quantum)

  4. Copy gopass-ssh-askpass.sh (stowed from dots-quantum to ~/.local/bin/)

  5. Test: gps v4/domains/d000/identity/ssh/github 'keys'

Validated Inventory

12 gopass v4 entries + 2 new (kvm-01, kvm-02):

Path Type Credential Groups

identity/ssh/github

ssh-key

ssh

identity/ssh/gitlab

ssh-key

ssh

identity/ssh/gitea

ssh-key

ssh

identity/ssh/id_ed25519_vault

ssh-key

ssh

identity/api/gitlab-pat

api-token

api

identity/ise/ise-01

appliance

admin, cli, dataconnect

identity/vault/cluster

service

api, operational, root-token, unseal

network/devices/firewall/vyos-01

firewall

api, ssh

network/devices/firewall/vyos-02

firewall

api, ssh

network/switch/3560cx-01

switch

api, console, enable, radius, snmp, ssh, tacacs

network/wlc/9800-wlc-01

wlc

ssh, web

network/wlc/9800-wlc-02

wlc

ssh, web

servers/kvm/kvm-01

server

ssh

servers/kvm/kvm-02

server

ssh

Entry Inventory
Path Type Status Notes

v4/domains/d000/identity/ssh/github

ssh-key-passphrase

Validated

Nested credentials.ssh confirmed

v4/domains/d000/identity/ssh/gitlab

ssh-key-passphrase

Validated

Nested credentials.ssh confirmed

v4/domains/d000/identity/ssh/gitea

ssh-key-passphrase

Validated

Nested credentials.ssh confirmed

v4/domains/d000/identity/ssh/id_ed25519_vault

ssh-key-passphrase

Validated

Nested credentials.ssh confirmed

v4/domains/d000/identity/api/gitlab-pat

api-token

Validated

Nested credentials.api confirmed

v4/domains/d000/identity/ise/ise-01

appliance

Validated

Nested credentials.{admin,cli,dataconnect} confirmed

v4/domains/d000/identity/vault/cluster

service

Validated

Nested credentials.{api,operational,root-token,unseal} confirmed

v4/domains/d000/network/devices/firewall/vyos-01

firewall

Validated

Reference entry — credentials.{api,ssh} confirmed

v4/domains/d000/network/devices/firewall/vyos-02

firewall

Validated

Nested credentials.{api,ssh} confirmed

v4/domains/d000/network/switch/3560cx-01

switch

Validated

Nested credentials.{api,console,enable,radius,snmp,ssh,tacacs} confirmed

v4/domains/d000/servers/kvm/kvm-01

server

Validated

Nested credentials.ssh confirmed — heredoc+tee tested

v4/domains/d000/servers/kvm/kvm-02

server

Validated

Nested credentials.ssh confirmed

gps Shell Function

YubiKey serial negotiation prompts bleed into stdout, corrupting direct pipes to yq. The gps function serializes decryption and parsing via temp file, with awk gate to strip prompt lines.

gps() {
  local tmp=$(mktemp)
  gopass show "$1" >| "$tmp" && {
    awk '/^[a-z].*: /{p=1} p' "$tmp" | yq "${@:2}"
  }
  rm -f "$tmp"
}

Mechanism:

  1. mktemp — creates temp file

  2. gopass show "$1" >| "$tmp" — decrypts to file (>| overrides zsh noclobber)

  3. awk '/^[a-z].*: /{p=1} p' — gate pattern: starts printing at first YAML key, drops all YubiKey/pinentry prompts before it

  4. yq "${@:2}" — all arguments after path passed as yq expression

  5. rm -f "$tmp" — cleanup (runs regardless of success)

Usage — structure audit (safe, no values):

gps v4/domains/d000/identity/ssh/github 'keys'
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | keys'
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | length'
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | to_entries[] | .key + " = " + (.value | type)'
gps v4/domains/d000/identity/ssh/github -r '.type + " → " + .service'
gps v4/domains/d000/identity/ssh/github '{type, service, domain, created}'
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | del(.passphrase)'

Usage — value extraction to clipboard:

gps v4/domains/d000/identity/ssh/github -r '.credentials.ssh.passphrase' | wl-copy
Extraction Patterns

Structure audit — keys only, no values exposed:

gopass show <path> | yq 'keys'
gopass show <path> | yq '.credentials | keys'
gopass show <path> | yq '.credentials.<group> | keys'
gopass show <path> | yq '.. | path | join(".")'

Value extraction — clipboard (no terminal exposure):

gopass show <path> | yq -r '.credentials.<group>.passphrase' | wl-copy
gopass show <path> | yq -r '.credentials.<group>.token' | wl-copy

Batch structure validation — all entries:

for entry in $(gopass ls --flat v4); do
  echo "=== $entry ==="
  gopass show "$entry" | yq 'keys' 2>/dev/null || echo "MALFORMED"
done
Validated Entry Commands
identity/ssh/github
gps v4/domains/d000/identity/ssh/github 'keys'
gps v4/domains/d000/identity/ssh/github '.credentials.ssh | keys'
gps v4/domains/d000/identity/ssh/github -r '.credentials.ssh.passphrase' | wl-copy
gps v4/domains/d000/identity/ssh/github -r '.credentials.ssh.key_file'
identity/ssh/gitlab
gps v4/domains/d000/identity/ssh/gitlab 'keys'
gps v4/domains/d000/identity/ssh/gitlab '.credentials.ssh | keys'
gps v4/domains/d000/identity/ssh/gitlab -r '.credentials.ssh.passphrase' | wl-copy
gps v4/domains/d000/identity/ssh/gitlab -r '.credentials.ssh.key_file'
identity/ssh/gitea
gps v4/domains/d000/identity/ssh/gitea 'keys'
gps v4/domains/d000/identity/ssh/gitea '.credentials.ssh | keys'
gps v4/domains/d000/identity/ssh/gitea -r '.credentials.ssh.passphrase' | wl-copy
gps v4/domains/d000/identity/ssh/gitea -r '.credentials.ssh.key_file'
identity/ssh/id_ed25519_vault
gps v4/domains/d000/identity/ssh/id_ed25519_vault 'keys'
gps v4/domains/d000/identity/ssh/id_ed25519_vault '.credentials.ssh | keys'
gps v4/domains/d000/identity/ssh/id_ed25519_vault -r '.credentials.ssh.passphrase' | wl-copy
gps v4/domains/d000/identity/ssh/id_ed25519_vault -r '.credentials.ssh.key_file'
identity/api/gitlab-pat
gps v4/domains/d000/identity/api/gitlab-pat 'keys'
gps v4/domains/d000/identity/api/gitlab-pat '.credentials.api | keys'
gps v4/domains/d000/identity/api/gitlab-pat -r '.credentials.api.token' | wl-copy
gps v4/domains/d000/identity/api/gitlab-pat -r '.credentials.api.scope'
gps v4/domains/d000/identity/api/gitlab-pat -r '.credentials.api.expires'
identity/ise/ise-01
gps v4/domains/d000/identity/ise/ise-01 'keys'
gps v4/domains/d000/identity/ise/ise-01 '.credentials | keys'
gps v4/domains/d000/identity/ise/ise-01 '.credentials.admin | keys'
gps v4/domains/d000/identity/ise/ise-01 '.credentials.cli | keys'
gps v4/domains/d000/identity/ise/ise-01 '.credentials.dataconnect | keys'

# Web GUI — copy password, open browser
gps v4/domains/d000/identity/ise/ise-01 -r '.credentials.admin.password' | wl-copy
# Username: admin — URL: https://ise-01.inside.domusdigitalis.dev

# CLI SSH — setsid required for keyboard-interactive
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh ise-01 2>&1 | tee /tmp/ise-01.log

# DataConnect (JDBC)
gps v4/domains/d000/identity/ise/ise-01 -r '.credentials.dataconnect.password' | wl-copy
gps v4/domains/d000/identity/ise/ise-01 -r '.credentials.dataconnect.port'
identity/vault/cluster
gps v4/domains/d000/identity/vault/cluster 'keys'
gps v4/domains/d000/identity/vault/cluster '.credentials | keys'
gps v4/domains/d000/identity/vault/cluster -r '.credentials.api.addr'
gps v4/domains/d000/identity/vault/cluster -r '.hostname + " (" + .ip + ")"'

# Web GUI — copy root token, open browser
gps v4/domains/d000/identity/vault/cluster -r '.credentials.root-token.token' | wl-copy
# URL: gps v4/domains/d000/identity/vault/cluster -r '.credentials.api.addr'
network/devices/firewall/vyos-01
gps v4/domains/d000/network/devices/firewall/vyos-01 'keys'
gps v4/domains/d000/network/devices/firewall/vyos-01 '.credentials | keys'
gps v4/domains/d000/network/devices/firewall/vyos-01 -r '.credentials.ssh.password' | wl-copy
gps v4/domains/d000/network/devices/firewall/vyos-01 -r '.credentials.api.key' | wl-copy
gps v4/domains/d000/network/devices/firewall/vyos-01 -r '.hostname + " (" + .ip + ")"'
network/devices/firewall/vyos-02
gps v4/domains/d000/network/devices/firewall/vyos-02 'keys'
gps v4/domains/d000/network/devices/firewall/vyos-02 '.credentials | keys'
gps v4/domains/d000/network/devices/firewall/vyos-02 -r '.credentials.ssh.password' | wl-copy
gps v4/domains/d000/network/devices/firewall/vyos-02 -r '.credentials.api.key' | wl-copy
gps v4/domains/d000/network/devices/firewall/vyos-02 -r '.hostname + " (" + .ip + ")"'
network/switch/3560cx-01
gps v4/domains/d000/network/switch/3560cx-01 'keys'
gps v4/domains/d000/network/switch/3560cx-01 '.credentials | keys'
gps v4/domains/d000/network/switch/3560cx-01 -r '.credentials.ssh.password' | wl-copy
gps v4/domains/d000/network/switch/3560cx-01 -r '.credentials.enable.password' | wl-copy
gps v4/domains/d000/network/switch/3560cx-01 -r '.hostname + " (" + .ip + ")"'

# CLI SSH — setsid required for keyboard-interactive
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh 3560cx-01 "show version | include uptime" \
  2>&1 | tee /tmp/3560cx-01.log
network/wlc/9800-wlc-01
gps v4/domains/d000/network/wlc/9800-wlc-01 'keys'
gps v4/domains/d000/network/wlc/9800-wlc-01 '.credentials | keys'

# Web GUI — copy password, open browser
gps v4/domains/d000/network/wlc/9800-wlc-01 -r '.credentials.web.password' | wl-copy
# Username: admin — URL: https://9800-wlc-01.inside.domusdigitalis.dev

# CLI SSH — setsid required for keyboard-interactive
gps v4/domains/d000/network/wlc/9800-wlc-01 -r '.credentials.ssh.password' | wl-copy
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh 9800-wlc-01 "show wireless client summary" \
  2>&1 | tee /tmp/wlc-01.log
network/wlc/9800-wlc-02
gps v4/domains/d000/network/wlc/9800-wlc-02 'keys'
gps v4/domains/d000/network/wlc/9800-wlc-02 '.credentials | keys'

# Web GUI — copy password, open browser
gps v4/domains/d000/network/wlc/9800-wlc-02 -r '.credentials.web.password' | wl-copy
# Username: admin — URL: https://9800-wlc-02.inside.domusdigitalis.dev

# CLI SSH — setsid required for keyboard-interactive
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh 9800-wlc-02 "show wireless client summary" \
  2>&1 | tee /tmp/wlc-02.log
servers/kvm/kvm-01
gps v4/domains/d000/servers/kvm/kvm-01 'keys'
gps v4/domains/d000/servers/kvm/kvm-01 '.credentials.ssh | keys'
gps v4/domains/d000/servers/kvm/kvm-01 -r '.credentials.ssh.password' | wl-copy
gps v4/domains/d000/servers/kvm/kvm-01 -r '.hostname + " (" + .ip + ")"'
servers/kvm/kvm-02
gps v4/domains/d000/servers/kvm/kvm-02 'keys'
gps v4/domains/d000/servers/kvm/kvm-02 '.credentials.ssh | keys'
gps v4/domains/d000/servers/kvm/kvm-02 -r '.credentials.ssh.password' | wl-copy
gps v4/domains/d000/servers/kvm/kvm-02 -r '.hostname + " (" + .ip + ")"'
Shell Integration
Current State
  • gps() function defined in session only — lost on terminal close

  • gopass-ssh-askpass.sh lives in domus-captures/scripts/ (documentation repo)

  • Existing gopass helpers in .zshrc: _gp_yaml, gpf, gpp, gpc, gpfields, gpsearch

  • Cross-shell modules load from ~/.config/shell/*.sh via array loop

Required Changes
Item Current Location Target Location

gps() function

Session-only (not persisted)

dots-quantum/zsh/.zshrc near existing gopass helpers OR ~/.config/shell/gopass.sh

gopass-ssh-askpass.sh

domus-captures/scripts/

dots-quantum/bin/.local/bin/gopass-ssh-askpass (stowed to ~/.local/bin/)

SSH_ASKPASS export

Manual per-session

dots-quantum/zsh/.zshrc (environment section)

Askpass Environment Setup
export SSH_ASKPASS=~/.local/bin/gopass-ssh-askpass
export SSH_ASKPASS_REQUIRE=force
Load All SSH Keys (askpass + loop)
export SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh
export SSH_ASKPASS_REQUIRE=force
for key in github gitlab gitea vault; do
  ssh-add ~/.ssh/id_ed25519_"$key"
done
Validated output (2026-06-30)
Identity added: /home/evanusmodestus/.ssh/id_ed25519_github (github-arch-2026)
Identity added: /home/evanusmodestus/.ssh/id_ed25519_gitlab (gitlab-arch-2026)
Identity added: /home/evanusmodestus/.ssh/id_ed25519_gitea (gitea-arch-2026)
Identity added: /home/evanusmodestus/.ssh/id_ed25519_vault (vault-signed-20260219)
Certificate added: /home/evanusmodestus/.ssh/id_ed25519_vault-cert.pub
Lifetime set to 06:21:35
Host-to-gopass Mapping (askpass script)
Host / IP gopass Path Credential Field

vyos-01 / 10.50.1.2

v4/domains/d000/network/devices/firewall/vyos-01

.credentials.ssh.password

vyos-02 / 10.50.1.3

v4/domains/d000/network/devices/firewall/vyos-02

.credentials.ssh.password

3560cx-01 / 10.50.1.10

v4/domains/d000/network/switch/3560cx-01

.credentials.ssh.password

9800-wlc-01 / 10.50.1.40

v4/domains/d000/network/wlc/9800-wlc-01

.credentials.ssh.password

9800-wlc-02 / 10.50.1.41

v4/domains/d000/network/wlc/9800-wlc-02

.credentials.ssh.password

kvm-01 / 10.50.1.110

v4/domains/d000/servers/kvm/kvm-01

.credentials.ssh.password

kvm-02 / 10.50.1.111

v4/domains/d000/servers/kvm/kvm-02

.credentials.ssh.password

ise-01 / 10.50.1.20

v4/domains/d000/identity/ise/ise-01

.credentials.cli.password

VyOS Vault SSH CA Trust
Objective

Enable Vault-signed SSH certificate authentication to VyOS firewalls. Primary auth via Vault cert, password fallback via gopass v4 extraction.

Investigation Timeline

Phase 1 — Firewall Access

VyOS SSH was reachable but DATA_LOCAL firewall chain restricted SSH (rule 20) to the ADMINS address group. Workstation IP 10.50.1.204 (wlan0, DHCP) was not in the group.

show configuration commands | grep "address-group ADMINS"
# Only contained 10.50.10.{106,107,108} — wrong subnet

Fix: added workstation IP to the ADMINS group.

configure
set firewall group address-group ADMINS address '10.50.1.204'
commit
save
exit

Phase 2 — Too Many Authentication Failures

First SSH attempt after firewall fix:

Received disconnect from 10.50.1.2 port 22:2: Too many authentication failures

Root cause: 20-d000-key-hierarchy.conf groups VyOS with all d000 hosts. SSH offers 5 identity files (Vault cert + 3 FIDO + 1 software key) before reaching password. VyOS MaxAuthTries (default 6) exhausted before password auth.

Visible in ssh -v output — 7 keys offered, all rejected:

debug1: Offering public key: id_ed25519_vault-cert.pub ED25519-CERT  ← rejected
debug1: Offering public key: id_ed25519_vault ED25519                ← rejected
debug1: Offering public key: id_ed25519_sk_rk_d000_nano ED25519-SK   ← rejected
debug1: Offering public key: id_ed25519_sk_rk_d000 ED25519-SK        ← rejected
debug1: Offering public key: id_ed25519_sk_rk_d000_secondary ED25519-SK ← rejected
debug1: Offering public key: id_ed25519_d000 ED25519                 ← rejected
Received disconnect: Too many authentication failures

Phase 3 — Vault CA Trust Configuration

VyOS CLI has no set service ssh trusted-user-ca-keys command (rolling 2026.03). Configured at the OpenSSH level directly.

Step 1 — Get the Vault SSH CA public key:

ds d000 dev/vault
vault read -field=public_key ssh/config/ca | tee /tmp/vault-ca.pub

Step 2 — Copy to VyOS (password auth, User=vyos):

ssh -o PreferredAuthentications=password -o User=vyos vyos-01 \
  "sudo tee /etc/ssh/vault-ca.pub" < /tmp/vault-ca.pub

Step 3 — Configure sshd on VyOS (virsh console):

# Add CA trust
sudo sh -c 'echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" >> /etc/ssh/sshd_config'

# Add principal mapping — cert principal "evanusmodestus" → local user "vyos"
sudo mkdir -p /etc/ssh/auth_principals
echo "evanusmodestus" | sudo tee /etc/ssh/auth_principals/vyos

# Add to sshd_config
sudo sh -c 'echo "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u" >> /etc/ssh/sshd_config'

# Verify
sudo sshd -T | grep -i "trusteduser\|principal"
# trustedusercakeys /etc/ssh/vault-ca.pub
# authorizedprincipalsfile /etc/ssh/auth_principals/%u

sudo systemctl restart ssh

Step 4 — Persist across VyOS image upgrades:

sudo mkdir -p /config/scripts/vyos-postconfig-bootup.d
sudo tee /config/scripts/vyos-postconfig-bootup.d/ssh-vault-ca.sh <<'SCRIPT'
#!/bin/bash
if [ -f /etc/ssh/vault-ca.pub ]; then
  grep -q "TrustedUserCAKeys" /etc/ssh/sshd_config || \
    echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" >> /etc/ssh/sshd_config
  grep -q "AuthorizedPrincipalsFile" /etc/ssh/sshd_config || \
    echo "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u" >> /etc/ssh/sshd_config
  mkdir -p /etc/ssh/auth_principals
  echo "evanusmodestus" > /etc/ssh/auth_principals/vyos
  systemctl restart ssh
fi
SCRIPT
sudo chmod +x /config/scripts/vyos-postconfig-bootup.d/ssh-vault-ca.sh

Phase 4 — Stale sshd Process

After configuring the CA, cert auth still failed. The AuthorizedPrincipalsFile was initially missing (never created on first attempt). After creating it, cert was still rejected.

Root cause: a stale sshd process held IPv4 port 22. The systemd-managed sshd only bound to IPv6 ([::]:22). Workstation connected via IPv4 (10.50.1.2) → hit the old sshd without the CA config.

sudo ss -tlnp | grep :22
# LISTEN  10.50.1.2:22  pid=2747571  ← STALE (old config)
# LISTEN     [::]:22    pid=2751415  ← NEW (has CA config)

Fix:

sudo kill 2747571
sudo systemctl restart ssh
sudo ss -tlnp | grep :22
# LISTEN  0.0.0.0:22  pid=2751605  ← same PID
# LISTEN     [::]:22   pid=2751605  ← same PID — correct

Phase 5 — Validated

Cert auth confirmed working:

ssh -F /dev/null -o User=vyos -i ~/.ssh/id_ed25519_vault 10.50.1.2 "hostname"
# Enter passphrase for key '/home/evanusmodestus/.ssh/id_ed25519_vault':
# vyos-01
Root Cause Summary
Issue Cause

Firewall blocked SSH

Workstation IP not in ADMINS address group

Too many auth failures

5 identity files offered before password; MaxAuthTries exhausted

Cert rejected despite CA config

AuthorizedPrincipalsFile missing — cert principal evanusmodestus didn’t map to local user vyos

Cert still rejected after principals fix

Stale sshd on IPv4 port 22 running old config; new sshd only bound to IPv6

VyOS CLI lacks trusted-user-ca-keys

Rolling 2026.03 — must configure at OpenSSH sshd_config level directly

SSH Config Fix Required

Remove vyos-01 vyos-02 from the main d000 host group in 20-d000-key-hierarchy.conf. Add a dedicated VyOS block:

Host vyos-01 vyos-02 vyos-vip
    User vyos
    IdentityFile ~/.ssh/id_ed25519_vault
    CertificateFile ~/.ssh/id_ed25519_vault-cert.pub
    IdentitiesOnly yes
    PasswordAuthentication yes
    PreferredAuthentications publickey,password

IdentitiesOnly yes — only the vault key is offered. No FIDO key exhaustion.

VyOS Remote Command Pattern

VyOS heredoc doesn’t work — vbash requires the operational mode wrapper. Use /opt/vyatta/bin/vyatta-op-cmd-wrapper for each command:

# Single command
ssh vyos-01 "/opt/vyatta/bin/vyatta-op-cmd-wrapper show version" 2>&1 | tee /tmp/vyos-01.log

# Multiple commands — chain with &&
ssh vyos-01 "/opt/vyatta/bin/vyatta-op-cmd-wrapper show version && \
  /opt/vyatta/bin/vyatta-op-cmd-wrapper show interfaces && \
  /opt/vyatta/bin/vyatta-op-cmd-wrapper show firewall group address-group ADMINS" \
  2>&1 | tee /tmp/vyos-01.log
vyos-02 Deployment Runbook

Step 1 — Open console (workstation terminal):

ssh kvm-02
sudo virsh console vyos-02

Step 2 — Enable SSH + firewall (vyos-02 console — paste this block):

Verify SSH service exists first:

show configuration commands | grep "service ssh"

If SSH is not configured:

configure
set service ssh port 22
commit
save
exit

Add workstation to ADMINS firewall group:

configure
set firewall group address-group ADMINS address '10.50.1.204'
commit
save
exit

Step 3 — Copy CA key (workstation terminal — verify SSH works first):

gps v4/domains/d000/network/devices/firewall/vyos-02 -r '.credentials.ssh.password' | wl-copy
ssh -o PreferredAuthentications=password -o User=vyos vyos-02 "sudo tee /etc/ssh/vault-ca.pub" < /tmp/vault-ca.pub

Step 4 — Configure sshd (vyos-02 console — paste this block):

sudo sh -c 'echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" >> /etc/ssh/sshd_config'
sudo mkdir -p /etc/ssh/auth_principals
echo "evanusmodestus" | sudo tee /etc/ssh/auth_principals/vyos
sudo sh -c 'echo "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u" >> /etc/ssh/sshd_config'

Step 5 — Persist across upgrades (vyos-02 console — paste this block):

sudo mkdir -p /config/scripts/vyos-postconfig-bootup.d
sudo tee /config/scripts/vyos-postconfig-bootup.d/ssh-vault-ca.sh <<'SCRIPT'
#!/bin/bash
if [ -f /etc/ssh/vault-ca.pub ]; then
  grep -q "TrustedUserCAKeys" /etc/ssh/sshd_config || \
    echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" >> /etc/ssh/sshd_config
  grep -q "AuthorizedPrincipalsFile" /etc/ssh/sshd_config || \
    echo "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u" >> /etc/ssh/sshd_config
  mkdir -p /etc/ssh/auth_principals
  echo "evanusmodestus" > /etc/ssh/auth_principals/vyos
  systemctl restart ssh
fi
SCRIPT
sudo chmod +x /config/scripts/vyos-postconfig-bootup.d/ssh-vault-ca.sh

Step 6 — Kill stale sshd + restart (vyos-02 console — paste this block):

sudo ss -tlnp | grep :22

If two different PIDs appear, kill the stale one:

sudo kill <stale_pid>

Then restart and verify single PID on both IPv4 and IPv6:

sudo systemctl restart ssh
sudo ss -tlnp | grep :22

Both lines must show the same PID. If IPv4 shows Address already in use, a stale process is still running — find and kill it.

Step 7 — Escape console (vyos-02 console):

Ctrl+] to exit virsh console, then exit from kvm-02.

Step 8 — Test cert auth (workstation terminal):

ssh vyos-02 "/opt/vyatta/bin/vyatta-op-cmd-wrapper show version" 2>&1 | tee /tmp/vyos-02-validated.log
Completed
  • Vault CA deployed on vyos-01

  • AuthorizedPrincipalsFile created — maps evanusmodestusvyos

  • Boot persistence script installed

  • Stale sshd killed, single PID confirmed on IPv4+IPv6

  • 20-d000-key-hierarchy.conf updated — VyOS block with IdentitiesOnly yes

  • LogLevel reverted to #LogLevel INFO

  • Cert auth validated: ssh vyos-01 connects without password

  • Remote command pattern validated: /opt/vyatta/bin/vyatta-op-cmd-wrapper

  • Repeat CA trust deployment on vyos-02

  • Test vyos-02 cert auth — ssh vyos-02 connects via Vault cert

Cisco Device SSH Access Pattern
Why Cisco Devices are Different

Cisco devices (ISE, 3560CX, 9800 WLC) do not support SSH key or certificate authentication. Password-only. They use keyboard-interactive auth, which means SSH_ASKPASS only works with setsid -w to detach the terminal.

SSH Config (already in dots-quantum)

Cisco devices are handled by two existing config files — no changes needed:

00-exceptions.conf — device-specific overrides:

Host 3560cx-01
    User admin
    PubkeyAuthentication no
    PasswordAuthentication yes
    KbdInteractiveAuthentication yes
    PreferredAuthentications keyboard-interactive,password

30-cisco-defaults.conf — glob pattern for all Cisco devices:

Host cisco-* switch-* router-* fw-* wlc-* 9800-wlc-* 9800-* 9300-* 3560cx-* ap-*
    User admin
    PubkeyAuthentication no
    PasswordAuthentication yes
    KbdInteractiveAuthentication yes
    PreferredAuthentications keyboard-interactive,password
    KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512,diffie-hellman-group14-sha256
    HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256
    Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes128-ctr
    MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
    ControlMaster no
Automated Login via Askpass

keyboard-interactive auth requires setsid -w to detach the terminal — without it, SSH prompts directly and ignores SSH_ASKPASS.

# ISE — interactive session (no remote command execution)
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh ise-01 2>&1 | tee /tmp/ise-01.log

# 3560CX — supports remote command execution
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh 3560cx-01 "show version | include uptime" \
  2>&1 | tee /tmp/3560cx-01.log

# 9800 WLC — supports remote command execution
SSH_ASKPASS=~/atelier/_bibliotheca/domus-captures/scripts/gopass-ssh-askpass.sh \
  SSH_ASKPASS_REQUIRE=force setsid -w ssh 9800-wlc-01 "show wireless client summary" \
  2>&1 | tee /tmp/wlc-01.log
Validated output — ISE (2026-06-30)
Warning: your password will expire in 21 days.
admin connected from 10.50.1.204 using ssh on ise-01
ise-01/admin#
Validated output — 3560CX (2026-06-30)
LAB-3560CX-01 uptime is 4 weeks, 2 days, 9 hours, 11 minutes
Validated output — 9800 WLC (2026-06-30)
Number of Clients: 3
14f6.d87b.3180 C9130AX-01  WLAN 4  Run  11ax(5)  Dot1x  Local
a059.50c9.92ee C9130AX-01  WLAN 5  Run  11ax(5)  MAB    Local
e0d5.5d6c.e166 C9130AX-01  WLAN 4  Run  11ax(5)  Dot1x  Local
Password Expiry

ISE admin password expires in 21 days (approx 2026-07-21). Gopass entry: v4/domains/d000/identity/ise/ise-01.credentials.cli.password

Rotation command (on ISE):

ise-01/admin# password

After rotation, update gopass:

gopass edit v4/domains/d000/identity/ise/ise-01
# Update credentials.cli.password value
gopass sync
Askpass Host Mapping

The askpass script maps ISE by hostname and IP:

ise-01 | 10.50.1.20 → v4/domains/d000/identity/ise/ise-01 → .credentials.cli.password

raices-grecolatinas

Proyecto creado: Raíces Grecolatinas

Nuevo proyecto de educación bajo data/d000/education/raices-grecolatinas/ — estudio sistemático de morfología léxica avanzada: raíces griegas y latinas que dominan el discurso moderno.

Origen: consulta sobre la distinción morfema vs. lexema (RAE) reveló la necesidad de un recurso propio para etimología aplicada — raíces como herramientas retóricas, no como curiosidad anticuaria.

Estructura: 12 capítulos organizados por dominio semántico, cada uno con raíces griegas y latinas, tablas de derivados modernos, y análisis de uso retórico/persuasivo:

Cap. Dominio

I

Poder y Autoridad (ἀρχή, κράτος, imperium, rex, potestas)

II

Mente y Conocimiento (γνῶσις, σοφία, νοῦς, λόγος, scientia, ratio)

III

Palabra y Discurso (λέξις, φωνή, ῥήτωρ, verbum, dictio, fari)

IV

Ley y Justicia (δίκη, νόμος, ius, lex)

V

Comercio y Riqueza (οἰκονομία, πλοῦτος, merx, pretium)

VI

Cuerpo y Medicina (σῶμα, ψυχή, θεραπεία, πάθος, corpus)

VII

Movimiento y Cambio (κίνησις, μεταβολή, motus, vertere)

VIII

Persuasión y Retórica (πειθώ, ἦθος, τρόπος, suadēre, captāre)

IX

Ciencia y Técnica (τέχνη, ἐπιστήμη, μέθοδος, ars, ingenium)

X

Guerra y Estrategia (στρατηγία, τακτική, bellum, vincere)

XI

Tiempo y Espacio (χρόνος, καιρός, τόπος, tempus, locus)

XII

Sociedad y Política (πόλις, δῆμος, civitas, res publica)

Formato: sigue EDUCATION-ASCIIDOC-STD — assembler + partials, [IMPORTANT.concept], [IMPORTANT.retorica], [TIP.gramatica], [qanda], tablas con stripes=even.

Complementa: retorica-study (Aristóteles/Cicerón), latin-study (LLPSI), estudios-quijotescos (Siglo de Oro), ciceron-study.

Libro de referencia recomendado: Soledad Varela Ortega — Morfología léxica: la formación de palabras (Gredos).

cd ~/atelier/_bibliotheca/domus-captures/data/d000/education/raices-grecolatinas && make

palaestra-cli-challenge

Palaestra Digitalis — CLI Challenge

Palaestra Digitalis — 2026-06-30
Theme: awk State Machines — Pattern Ranges and Flags

Yesterday you conquered find -regex (full-path gotcha, -regextype posix-extended). Today: awk beyond field extraction — the patterns that make awk replace sed + grep + head + tail in a single pass.

The Two Idioms
Idiom 1 — Range Pattern (built-in)
awk '/START/,/END/' file (1)
1 Prints every line from the first match of /START/ through the first match of /END/, inclusive. Like sed -n '/START/,/END/p' but you stay in awk and can add field processing.
Idiom 2 — Flag-Based State Machine (manual)
awk '/START/{p=1} p; /END/{p=0}' file (1)
awk '/START/{p=1; next} /END/{p=0; next} p' file (2)
1 Inclusive — prints START and END lines. The flag p gates output.
2 Exclusive — skips START and END lines. next jumps to the next input line.
When to use which

Range patterns (/S/,/E/) are concise but inflexible — you cannot exclude the delimiters, cannot nest, and the range restarts on every new match of START. The flag idiom gives you full control: include/exclude boundaries, nest ranges, accumulate state across blocks.

Rule of thumb: if you need to transform what’s inside the range, use the flag. If you just need to extract, the range pattern suffices.

Real-World Drills — Your Infrastructure
Drill 1 — Extract a [Section] from ISE backup config

ISE backup configs (and wpa_supplicant.conf, sshd_config, etc.) use [Section] headers. Extract one section:

# Extract the [EAP-TLS] section from a wpa_supplicant conf
awk '/^\[EAP-TLS\]/{p=1; next} /^\[/{p=0} p' /etc/wpa_supplicant/wpa_supplicant-wired.conf

The trick: /^\[/ (any section header) terminates the range. This is why the flag idiom beats the range pattern — the "end" marker is structural, not a fixed string.

Drill 2 — Extract a YAML block by key
# From antora.yml: extract all lines under "asciidoc:" until the next top-level key
awk '/^asciidoc:/{p=1} p && /^[a-z]/ && !/^asciidoc:/{p=0} p' docs/antora.yml
Drill 3 — Multi-file comparison with FNR/NR
# Compare line counts across all nav partials
awk 'FNR==1{if(NR>1) printf "%6d  %s\n", last, prev; prev=FILENAME} {last=FNR} \
  END{printf "%6d  %s\n", last, prev}' \
  docs/modules/ROOT/partials/nav/*.adoc (1)
1 FNR resets per file; NR is global. FILENAME gives the current file. The FNR==1 block prints the previous file’s count when a new file begins — END catches the last one. One awk invocation replaces wc -l + for loop.
Drill 4 — Count items per section
# How many nav entries per nav partial?
awk '/^\*/{c++} FNR==1{if(NR>1) printf "%4d  %s\n", c, prev; c=0; prev=FILENAME} \
  END{printf "%4d  %s\n", c, prev}' docs/modules/ROOT/partials/nav/*.adoc
Drill 5 — State machine: extract fenced code blocks from AsciiDoc
# Extract all source blocks from any .adoc file
awk '/^\[source,/{lang=$0; next} /^----$/{if(p){p=0;print "---"} else {p=1;print lang}} p' \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-29/build-and-open-commands.adoc

This is a two-state machine: outside code (p=0) and inside code (p=1). The ---- delimiter toggles the state. lang captures the language annotation from the preceding line.

Bonus Drill — Process Inspection with /proc
Drill 6 — Find all Claude Code sessions

ps aux | awk breaks when column positions shift or format specifiers silently return empty. The kernel’s /proc filesystem is the authoritative source — no userspace formatting layer to fail.

# Claude Code sessions — PID, age, working directory
for pid in $(pgrep -x claude); do
  printf "PID %-8s  Since: %s  CWD: %s\n" \
    "$pid" \
    "$(stat -c '%Y' /proc/$pid | xargs -I{} date -d @{} '+%Y-%m-%d %H:%M')" \
    "$(readlink /proc/$pid/cwd)"
done

Why this works better than ps:

Technique Why

pgrep -x claude

Exact match on process name — no [c]laude bracket hack to exclude grep from its own results

stat -c '%Y' /proc/$pid

Start time from the kernel directly — ps -o lstart= silently returns empty for some process contexts

readlink /proc/$pid/cwd

Working directory identifies the session — more useful than the command name

for + printf

Structured output, no field-position fragility from awk '{print $11}' on variable-width columns

When to use which process tool

ps — human-readable snapshots, quick glances. Breaks when you parse it programmatically because column widths are variable and format specifiers are OS-dependent.

/proc — the kernel’s live view. Always authoritative. stat, readlink, cat /proc/$pid/status give you structured data that won’t shift under your feet.

pgrep/pkill — process selection by name or pattern. Use -x for exact, -f for full command line. Replaces ps aux | grep | grep -v grep.

ss/lsof — when you need sockets or file descriptors, not processes. "What’s listening on port 8080?" is ss. "What process is this?" is ps/pgrep//proc.

Bonus Drill — Process Substitution for Read-Only Inspection

Process substitution (<(…​)) creates a FIFO in /dev/fd/ — the consumer gets a file descriptor, not a path. Neovim reads it into a buffer, but there’s no backing file to save to. This is read-only inspection of ephemeral output with full vim motions.

Drill 7 — Diff nftables rulesets across time
# Step 1: Snapshot
sudo nft list ruleset > /tmp/nft-before.txt
# Step 2: Make a change
sudo nft add rule ip filter DOCKER-USER tcp dport 9999 drop
# Step 3: Diff — the new rule shows up highlighted
nvim -d /tmp/nft-before.txt <(sudo nft list ruleset)
# Step 4: Clean up the test rule
sudo nft delete rule ip filter DOCKER-USER handle \
  $(sudo nft -a list chain ip filter DOCKER-USER | awk '/9999/{print $NF}')

Each step runs separately. Step 3 opens an interactive diff — nvim -d blocks until you quit. Without step 2, both sides are identical and no diff hunks appear.

Finding: Docker owns the entire nftables ruleset

Running sudo nft list ruleset revealed that every chain on this system is Docker-managed:

Table Purpose

ip filter

DOCKER, DOCKER-FORWARD, DOCKER-BRIDGE, DOCKER-CT, DOCKER-USER chains. Forward policy is DROP.

ip nat

PREROUTING DNAT for port 18000 → 172.18.0.3:8000 (container on br-36c84bbc5e1c). POSTROUTING MASQUERADE for 172.17.0.0/16 and 172.18.0.0/16.

ip raw

Anti-spoof rules dropping traffic to container IPs from outside their bridge.

ip6 filter/nat

IPv6 Docker scaffolding — empty chains, no custom rules.

What’s missing: No inet filter INPUT chain for the host. No SSH rate limiting. No ICMP policy. The DOCKER-USER chain is empty — that’s where custom forwarding rules should go (Docker won’t overwrite it on restart). The systems-mastery phase 6 exercise (nft-basic.sh) adds an inet filter table that coexists with Docker’s ip filter.

Drill 8 — AppArmor profile inspection
# Diff two AppArmor profiles locally
nvim -d /etc/apparmor.d/firefox /etc/apparmor.d/chrome

# Review enforcement status — aa-status requires sudo with a live TTY
# Do NOT use 2>/dev/null — it eats the password prompt and returns empty
sudo aa-status | nvim -

# List profiles by enforcement mode with awk
sudo aa-status | awk '/^[0-9]+ profiles/{mode=$NF} /^ /{print mode, $0}'
Gotcha: sudo inside process substitution

nvim <(sudo aa-status 2>/dev/null) opens an empty buffer. The process substitution runs in a subshell — sudo cannot prompt for a password because there’s no controlling TTY. The 2>/dev/null silently eats the error, so you see nothing.

Fix: Run sudo first, pipe to nvim - (stdin). Or authenticate beforehand with sudo -v so the credential cache is warm, then nvim <(sudo aa-status) works.

Profile names on Arch are just firefox, chrome, brave — not the usr.bin.firefox Debian/Ubuntu convention.

Drill 9 — Git branch divergence without checkout
# Diff a file between two branches — no checkout, no stash, no risk
nvim -d <(git show main:Makefile) <(git show feature:Makefile)

# Compare commit logs between two repos side by side
nvim -d \
  <(git -C ~/atelier/_bibliotheca/domus-captures log --oneline -20) \
  <(git -C ~/atelier/_bibliotheca/domus-docs log --oneline -20)

git show branch:path extracts a file at a specific revision to stdout. Process substitution turns two of these into a diff view — zero working tree mutations.

Drill 10 — Inspect listening services
# Review all listening sockets with full process context
nvim <(ss -tlnp)

# Compare listening ports now vs a known-good baseline
nvim -d baseline-ports.txt <(ss -tlnp)

Your system right now: SSH on 22, CUPS on 631, Ollama on 11434, ADB on 5037. If something new appears, the diff catches it.

Drill 11 — Audit Docker and systemd state
# Review all running containers with full detail
nvim <(docker ps --no-trunc --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}')

# Diff systemd service state between boots
nvim -d \
  <(systemctl list-units --type=service --state=running --no-pager) \
  <(journalctl -b -1 -u '*.service' --no-pager | awk '/Started/{print $NF}' | sort -u)
Drill 12 — Review fail2ban and audit logs
# Warm the sudo cache first, then process substitution works
sudo -v

# Inspect fail2ban jail status — which IPs are banned right now
nvim <(sudo fail2ban-client status sshd)

# Review recent audit events with full context
sudo ausearch -ts recent --format text | nvim -

# Diff audit rules: what's loaded vs what's on disk
nvim -d <(sudo auditctl -l) <(cat /etc/audit/rules.d/*.rules)

The sudo -v trick: it prompts once with a real TTY, then subsequent sudo calls in subshells use the cached credential. The audit diff catches drift — rules added at runtime that never made it to disk, or disk rules that failed to load.

Drill 13 — Ephemeral scratchpad (no disk, writable)
# When you need to *edit*, not just *read* — buftype=nofile lives in memory only
nvim +"setlocal buftype=nofile" +"put ='scratch content'"

# Pipe result out without ever saving a file
# Inside nvim: :%w !wl-copy     (copies buffer to clipboard)
# Inside nvim: :%w !awk '{...}' (process buffer through awk)
Process substitution vs buftype=nofile

nvim <(command) — read-only inspection. The buffer is backed by a drained FIFO. You can search, yank, filter — but :w has no target. Best for: diffing ephemeral outputs, reviewing command results with vim motions.

nvim +"setlocal buftype=nofile" — writable scratchpad. Lives only in memory. :w refuses unless you give an explicit path. Best for: composing text you’ll pipe elsewhere, transforming clipboard content, ad-hoc calculations.

nvim -d <(a) <(b) — the strongest use case. Two ephemeral streams in a diff view with folded unchanged regions, syntax highlighting, and yank-from-diff. Replaces diff <(a) <(b) when you need more than pass/fail.

Progression Map
Day Focus Status

06-29

find -regex, process substitution, -regextype

Done

06-30

awk state machines — range patterns, flag idioms, FNR/FILENAME

Today

07-01

awk arrays — frequency counting, BEGIN/END reports

Next

07-02

sed hold buffer — multi-line transforms

Upcoming

Ad-Hoc Requests

Ad-Hoc Requests

Capture walk-ups, Teams pings, and unplanned work here.

*

ScrubEx Dispenser — Slow Web Service Downloads
Context

ScrubEx automated scrub/linen dispensers (embedded Linux, Debian-based) make web service calls every 20 seconds for inventory sync, authentication, and remote diagnostics. Vendor reports "random" slow downloads. No blocks observed on FTD. The 802.1X port has no dACL applied.

Findings So Far
  • Traffic is allowed — not being dropped by firewall or NAC

  • The problem is latency on download, not connection failure

  • "Random" correlates with response payload size, not time-of-day (hypothesis — needs validation)

Suspected Root Cause: MTU Fragmentation

Hospital network paths commonly reduce effective MTU below 1500 due to encapsulation:

Encapsulation Overhead Effective MTU

VXLAN (ACI / campus fabric)

-50 bytes

1450

GRE tunnel

-24 bytes

1476

IPsec VPN

-52 to -73 bytes

1427–1448

802.1Q VLAN tag

-4 bytes

1496

Stacked QinQ

-8 bytes

1492

If the dispenser’s eth0 is set to MTU 1500 but the path only supports 1450, every response over ~1400 bytes fragments. Embedded Linux with limited RAM reassembles slowly. Small requests (badge auth, heartbeat) fit in one packet and work fine — large responses (inventory manifest) fragment and appear slow.

Validation Steps
# Step 1: Find the MTU ceiling — run from a host on the same VLAN as the dispenser
ping -M do -s 1472 <dispenser-ip>

If that fails, find the exact break point:

# Step 2: Binary search for max MTU
for size in 1400 1420 1440 1450 1460 1470 1472; do
  printf "%-6s " "$size"
  ping -M do -s $size -c 1 -W 1 <dispenser-ip> 2>&1 \
    | awk '/bytes from/{print "OK"} /too long|frag/{print "FAIL"} /timeout/{print "TIMEOUT"}'
done
# Step 3: Check interface stats on the dispenser (if SSH access)
ip -s link show eth0
ethtool -S eth0 | grep -iE 'drop|error|miss|frag'
# Step 4: Capture a slow download cycle — look for fragmentation and TLS renegotiation
sudo tcpdump -i <iface> host <dispenser-ip> -w /tmp/scrubex-$(date +%F).pcap -c 5000

In Wireshark, filter:

  • ip.flags.mf == 1 — fragmented packets

  • tcp.analysis.retransmission — retransmits (drops)

  • tls.handshake.type == 1 — ClientHello (TLS renegotiation every 20s = expensive on embedded)

  • dns — DNS query spam (resolving hostname every call instead of caching)

Other Possibilities
Cause How to Confirm

TLS renegotiation every call

tcpdump — look for ClientHello every 20s instead of Application Data on a reused session

DNS lookup every call

tcpdump port 53 — count DNS queries per 20s cycle

Payload bloat (full sync vs delta)

Check response Content-Length — if it returns the full inventory every poll, that’s the vendor’s problem

CPU bottleneck on the dispenser

top -b -n 3 -d 20 on the dispenser during poll cycles

Fix (if MTU confirmed)
# Temporary — on the dispenser
sudo ip link set eth0 mtu 1400

# Permanent — Debian /etc/network/interfaces
# Add under the iface block: mtu 1400
Status
Item Detail Status

FTD drops

None observed

Ruled out

dACL on 802.1X port

None applied

Ruled out

MTU fragmentation

Suspected — needs ping -M do test

Pending

TLS renegotiation

Needs pcap analysis

Pending

Vendor response payload size

Ask vendor for Content-Length on inventory sync

Pending

ADB Mobile Deployment — AsciiDoc Projects

Deploy a built AsciiDoc project (HTML + SVG assets) to an Android device for offline reading.

Prerequisites
  • Device connected via USB: adb devices -l

  • Project already built: make build-data PROJ=<slug>

Workflow
# Step 1: Clean staging directory
rm -rf /tmp/<slug>-mobile && mkdir -p /tmp/<slug>-mobile
# Step 2: Copy built HTML
cp data/d000/education/<slug>/output/<slug>.html /tmp/<slug>-mobile/
# Step 3: Copy all SVG assets preserving relative paths
find data/d000/education/<slug>/partials -name '*.svg' | while read f; do
  relpath=$(echo "$f" | sed 's|.*/<slug>/||')
  mkdir -p "/tmp/<slug>-mobile/$(dirname $relpath)"
  cp "$f" "/tmp/<slug>-mobile/$relpath"
done
# Step 4: Fix image paths — absolute to relative
sed -i 's|data/d000/education/<slug>/||g' /tmp/<slug>-mobile/<slug>.html
# Step 5: Push to device
adb push /tmp/<slug>-mobile/ /sdcard/Download/<slug>-mobile/
Example — Estudios Quijotescos
rm -rf /tmp/quijote-mobile && mkdir -p /tmp/quijote-mobile

cp data/d000/education/estudios-quijotescos/output/estudios-quijotescos.html \
  /tmp/quijote-mobile/

find data/d000/education/estudios-quijotescos/partials -name '*.svg' | while read f; do
  relpath=$(echo "$f" | sed 's|.*/estudios-quijotescos/||')
  mkdir -p "/tmp/quijote-mobile/$(dirname $relpath)"
  cp "$f" "/tmp/quijote-mobile/$relpath"
done

sed -i 's|data/d000/education/estudios-quijotescos/||g' \
  /tmp/quijote-mobile/estudios-quijotescos.html

adb push /tmp/quijote-mobile/ /sdcard/Download/quijote-mobile/
Verified On
Device Date

Samsung Z Fold 7 (SM-F966U1)

2026-06-30

Open on the phone: Files → Download → quijote-mobile → estudios-quijotescos.html


Work (CHLA)

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

Critical (P0)

Project Description Owner Status Due Blocker

Linux Research (Xianming Ding)

EAP-TLS for Linux workstations, dACL, UFW

Evan

BEHIND (72 days overdue)

02-24

Certificate "password required" - nmcli fix documented

iPSK Manager

Pre-shared key automation

Ben Castillo

BEHIND

 — 

DB replication issues

MSCHAPv2 Migration

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

Evan

25% — Report due, batch queries ready

05-30

Report to turn in

Research Segmentation

All endpoints to Untrusted VLAN

Evan

BLOCKED

 — 

CISO decision pending

Disaster Recovery

ISE DR scoping — dot1x closed mode = total blackout

Evan

Scoping

 — 

 — 

Mandiant Remediation

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

Evan

Active

 — 

 — 

SIEM QRadar → Sentinel

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

Evan

Active — blocked on DCR

Q2 2026

Victor/Mauricio: create DCR, resolve Azure network policy

Abnormal Security

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

Evan

Active — CAB today 15:00

05-14

Pre-CAB checklist: confirm Tyler, Jason, Sarah

High Priority (P1)

Project Description Owner Status Target

ISE 3.4 Migration

Upgrade from 3.2p9

Evan

Blocked — maintenance window needed

Q2 2026

Switch Upgrades

IOS-XE fleet update (C9300, 3560CX)

Evan

Pending

Q2 2026

Spikewell BYOD VPN

dACL SQL, AD group integration

Evan

Active

 — 

Strongline Gateway

MAC capture, Identity Group setup — 37 days aging

Evan

Active — David Rukiza assigned

 — 

Abnormal Security

AI email security platform research, ESA cutover timeline

Evan

Newly assigned

 — 

DMZ Migration

External services audit behind NetScaler

Evan

Audit phase

 — 

Firewall Audit (murus-portae)

EtherChannel query, prefilter, policy assignments

Evan

Scoping — ASA API creds needed

 — 

iPSK Manager HA

Server 2 config, TLS, SQL security audit

Evan

In progress

 — 

Sentinel KQL

Build proficiency, distinguish from team

Evan

Onboarding

 — 

VNC Blocking

Block and eliminate VNC protocol enterprise-wide

Evan

Active — Phase 0 (Discovery)

Mid-June 2026

Strategic (P2)

Project Description Owner Status

HHS Regulatory Compliance

New HHS security policies implementation

TBD

NOT STARTED

InfoSec Reporting Dashboard

PowerBI metrics for executives

TBD

NOT STARTED

EDR Migration (AMP → Defender)

Endpoint protection consolidation

TBD

NOT STARTED

Azure Legacy Migration

Modern landing zone

Team

In Progress

ChromeOS EAP-TLS

SCEP + Victor, Paul testing

Victor

In Progress

P0 — Critical / Blocking

Security & Compliance

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

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

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

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

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

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

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

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

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

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

    • Document RTO/RPO per system

SIEM Migration (QRadar → Sentinel)

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

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

    • Dependency: Monad pipeline for log source transition

    • Dependency: Sentinel KQL proficiency for query migration

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

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

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

Active Deployments & Migrations

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

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

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

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

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

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

  • Enterprise Linux 802.1X — standardize Shahab/Ding deployment (CISO priority). Overdue since 02-24. Blocked by nmcli cert fix. 06-15: Assembler build fixed (26 errors → 0). Segmentation document extension to 06-16. d001 open linux-research to work on it.

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

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

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

Tube System Upgrade (NEW — 06-01)

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

BMS Controller Segmentation (MIGRATED — 06-09)

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

BMS Device Inventory (NEW — 04-24)

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

VNC Blocking (NEW — 05-11)

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

Investigations & Audits

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

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

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

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

Stale Blockers (carried via carryover tracker)

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

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

Administrative

  • PeopleSoft — track time for current week

  • iTrack tickets — close open tickets

  • KQL library — build initial queries in codex + d001

  • Linux Research project — finalize and review

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

P1 — Important

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

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

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

  • Vocera/Wyse iTrack RCA — complete root cause report

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

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

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

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

  • ISE 3.4 Migration — depends on Patch 10 completion first

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

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

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

Infrastructure (Personal)

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

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

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

  • Libvirt VLAN hook debug on both KVMs

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

  • Vault Raft cluster — verify vault-01 rejoined

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

Completed (confirmed — do not delete, archive only)

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

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

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

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

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

Service Requests (SR)

SR# Request Requestor Opened Status

3508542

Zoll cards connection issue

STALE — verify in iTrack

3508524

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

STALE — verify in iTrack (issues persisted after disable)

3528165

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

Genest, John (vendor contact)

2026-06-01

NEW — MACs received, need ISE onboarding

Incidents (INC)

INC# Priority Description Opened SLA Status

1911859

Strongline Gateways in Miscellaneous Subnet

STALE — verify in iTrack (related to carryover P0)

Change Requests - Emergency (ECAB)

CR# Description Opened Scheduled Status

No emergency changes

Change Requests - Normal

CR# Description Opened Scheduled Status

No normal changes

Change Requests - Scheduled/Standard

CR# Description Opened Window Status

No scheduled changes

Change Requests - Root Cause / Post-Incident

CR# Description Related INC Opened Status

100451

Vocera Phones and Wyse devices went off network

STALE — verify in iTrack


Session Accomplishments (Claude Code)

Day-specific accomplishments here.


Personal

In Progress

Project Description Status Notes

k3s Platform

Production k3s cluster on kvm-01

Active

Prometheus, Grafana, Wazuh deployed

Wazuh Archives

Enable archives indexing in Filebeat

Active

PVC fix pending

kvm-02 Hardware

Supermicro B deployment

Active

Hardware ready, RAM upgrade done

Planned

Project Description Target Blocked By

Vault HA (3-node)

vault-02, vault-03 on kvm-02

Q2 2026 (slipped from Q1)

kvm-02 deployment

k3s HA (3-node)

Control plane HA

Q2 2026 (slipped from Q1)

kvm-02 deployment

ArgoCD GitOps

k3s GitOps deployment

After k3s stable

 — 

MinIO S3

Object storage for k3s

After ArgoCD

 — 

Domus Inventory

Personal asset management (YAML + CLI + AsciiDoc)

Q2 2026

Schema approved

Active — Infrastructure

Task Details Priority Status Due

Wazuh agent deployment

Deploy agents to all infrastructure hosts

P2

Pending

After archives fix

k3s Platform

Production k3s cluster on kvm-01

P1

In Progress

 — 

Wazuh Archives

Enable archives indexing in Filebeat, PVC fix

P1

In Progress

 — 

kvm-02 Hardware

Supermicro B deployment, RAM upgrade done

P1

In Progress

 — 


Active — Security & Encryption

Task Details Priority Status Due

Configure 4th YubiKey

SSH FIDO2 keys

P1

TODO

 — 

Cold storage M-DISC backup

age-encrypted archives

P1

TODO

After YubiKey setup


Active — Development & Tools

Task Details Priority Status Due

netapi Commercialization

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

P0

Active

 — 

Ollama API Service

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

P0

Active

 — 

Shell functions (fe, fec, fef)

File hunting helpers

P3

TODO

 — 


Active — Documentation

Task Details Priority Status Due

D2 Catppuccin Mocha styling

domus-* spoke repos (177 files total)

P3

In Progress

 — 


Active — Financial

Task Details Priority Status Due

Amazon order history import

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

P1

Waiting

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


Active — Education

Task Details Priority Status Due

No active education tasks — see education trackers


Active — Personal & Life Admin

Task Details Priority Status Due

ThinkPad T16g Setup

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

P0

Pending

 — 

P50 Arch to Ubuntu migration

CR-2026-03-12

P2

In Progress

 — 

X1 Carbon Ubuntu installs

2 laptops, LUKS encryption

P2

In Progress

 — 

P50 Steam Test

Test Flatpak Steam + apt cleanup of broken i386 packages

P3

Pending

 — 

Documentation Sites

Notes

Day-specific personal notes here.


Education

Claude Code Mastery

Resource Details Progress Status

Claude Code Full Course (4 hrs)

Nick Saraev - YouTube comprehensive course

26:49 / 4:00:00

IN PROGRESS

Claude Code Certification

Anthropic official certification (newly released)

Not started

GOAL

Skills Mastery (Critical)

Certification Deadlines

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

  • RHCSA 9 - Q3 2026 (after CISSP)

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

Spanish C1 Certification Goals

Certification Provider Target Status Strategy

SIELE C1

Instituto Cervantes / UNAM / Salamanca

Q2 2026

ACTIVE

Computer-based, faster results - take FIRST

DELE C1

Instituto Cervantes

Q3/Q4 2026

PLANNED

After SIELE success, harder exam

DELE C2

Instituto Cervantes

2027

FUTURE

Mastery level - requires extensive immersion

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

Don Quijote Writing Practice - DELE C1/C2 Initiative

Method:

  1. Read chapter in original Spanish

  2. Write personal analysis/understanding en espanol

  3. AI review for grammar, vocabulary, register

  4. Build comprehensive understanding of literary elements

Today’s Study

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

  • Secondary: RHCSA curriculum, Spanish SIELE C1

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

  • RHCSA — continue curriculum phase

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

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

Regex Training (CRITICAL)

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

  • Priority: After PeopleSoft, before Quijote

  • Session: Character classes, word boundaries


Infrastructure

Documentation Sites

Site URL Status Actions Needed

Domus Digitalis

docs.domusdigitalis.dev

Active

Validate, harden, improve

Architectus

docs.architectus.dev

Active

Public portfolio site - maintain

HA Deployment Status

System Description Status Notes

VyOS HA

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

✅ COMPLETE

2026-03-07 - pfSense decommissioned

BIND DNS HA

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

✅ COMPLETE

Zone transfer operational

Vault HA

Raft cluster (vault-01/02/03)

✅ COMPLETE

Integrated with PKI

Keycloak Rebuild

keycloak-01 corrupted, rebuild from scratch

🔄 NEXT

Priority P3 - SSO broken

FreeIPA HA

ipa-02 replica planned

📋 PLANNED

Linux auth redundancy

AD DC HA

home-dc02 replication

📋 PLANNED

Windows auth redundancy

iPSK Manager HA

ipsk-mgr-02 with MySQL replication

📋 PLANNED

PSK portal redundancy

ISE HA

PAN HA (ise-01 reconfigure)

⏳ DEFERRED

Wait until ise-02 stable

ISE 3.5 Migration

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

📋 PLANNED

After 3.4 Migration completes (Q2 2026)

Single Points of Failure (CRITICAL)

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

ISE (ise-02)

All 802.1X stops - wired and wireless auth fails

ise-01 reconfiguration deferred until ise-02 stable

Keycloak (keycloak-01)

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

NEXT PRIORITY - Rebuild runbook

FreeIPA (ipa-01)

Linux auth, sudo rules, HBAC fails

ipa-02 replica planned

AD DC (home-dc01)

Windows auth, Kerberos, GPO fails

home-dc02 replica planned

iPSK Manager

Self-service PSK portal unavailable

ipsk-mgr-02 with MySQL replication planned

Validation Tasks

Task Details Status

docs.domusdigitalis.dev validation

Test all cross-references, search, rendering

TODO

docs.domusdigitalis.dev hardening

HTTPS, CSP headers, security review

TODO

docs.architectus.dev validation

Public site content review

TODO

Hub-spoke sync verification

All components building correctly

Ongoing


Quick Commands

Git & GitHub CLI

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

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

gh CLI — repo discovery and filtering

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

find & grep

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

Search codex by content — which files contain a command?

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

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

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

find → grep → open in nvim

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

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

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

Trace Antora partial inclusion chains

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

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

Multi-pattern file search — worklog partial discovery

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

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

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

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

Cross-repo literary term search — bibliotheca-wide discovery

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

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

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

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

Search daily notes — find commands, builds, and patterns across worklogs

Source: 2026-06-22 — searching for build-antora-page invocations and theme usage across partials

which daily note partials mention a command?
# Find files — filenames only
grep -rl 'build-antora-page' docs/modules/ROOT/partials/worklog/
see the match in context without opening the file
# 3 lines of context around each hit
grep -rn -C3 'build-antora-page' docs/modules/ROOT/partials/worklog/
search for any theme flag across all partials (PCRE alternation)
grep -rPn 'theme\s+(light-cyan|catppuccin|mocha)' docs/modules/ROOT/partials/
find + grep — search all daily note partials for build/export patterns
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec grep -l 'build-antora-page\|build-adoc\|--theme\|pdf' {} \;
find + awk — show filename:matching_line for every hit (no -r needed)
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec awk '/build-antora-page|build-adoc|--theme/{print FILENAME": "$0}' {} \;
awk state machine — extract full [source,bash] blocks containing a command
find docs/modules/ROOT/partials -name '*.adoc' -exec \
  awk '/^\[source,bash\]/{block=1; buf=""} \
       block{buf=buf"\n"$0} \
       /^----$/ && block>1{if(buf~/build-antora-page/) print FILENAME":"buf; block=0} \
       block{block++}' {} \;

The awk state machine: [source,bash] sets block=1 and starts buffering. Each line appends to buf. When the closing ---- arrives (and block>1 to skip the opening fence), check if the buffer contains the target command. If yes, print filename + entire block. Reset. One pass, arbitrary block size, no temp files.

sed address range — print only lines between code fences matching a pattern
find docs/modules/ROOT/partials/worklog -name '*.adoc' -exec \
  sed -n '/\[source,bash\]/,/^----$/{/build-antora-page/p}' {} +
search data/ too — standalone adoc outside Antora
grep -rn 'build-antora-page\|build-adoc' data/ --include='*.adoc'
every invocation sorted by path (chronological by date directory)
grep -rn 'build-antora-page' docs/modules/ROOT/partials/worklog/daily-notes/ \
  | awk -F: '{print $1, $3}' | sort

Pattern escalation: grep -rl → "where is it". grep -rn -C3 → "what’s around it". find -exec awk → "extract the structured block". sed address ranges → "print between delimiters". Each tool has a different affordance — grep finds, awk extracts structure, sed filters ranges, find -exec scales across trees.

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

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

comm — set difference (who hasn’t replied)

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

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

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

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

Sort find results by modification time (newest first)

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

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

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

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

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

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

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

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

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

File intelligence — size, type, duplicates, age

Beyond finding files — interrogating them.

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

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

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

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

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

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

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

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

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

Batch operations — rename, move, transform

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

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

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

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

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

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

xargs power patterns

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

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

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

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

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

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

Process substitution — diff without temp files

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

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

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

awk, sed, jq

awk — field extraction

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

sed — stream editing

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

jq — JSON processing

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

Shell Patterns

xargs — when the next command reads arguments, not stdin

Next command reads…​ Use

stdin (awk, grep, wc, sort)

pipe directly

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

xargs

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

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

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

Command substitution — embed output as arguments

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

Conditional execution — capture, test, act

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

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

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

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

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

tee_clean — color on screen, clean text in file

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

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

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

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

Dependency check — verify toolchain in one shot

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

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

printf safety — dashes as data, not options

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

Kill stuck SSH sessions

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

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

File Descriptors & Redirection

The three file descriptors

FD Name Purpose

0

stdin

input to the command

1

stdout

normal output (valid results)

2

stderr

error messages

Split stdout and stderr into separate files

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

Suppress errors — 2>/dev/null

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

Merge stderr into stdout — 2>&1

command 2>&1 | grep "pattern"

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

Heredoc patterns

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

Multi-line description here.
EOF
)"

API & curl/jq

domus-api — Documentation System REST API

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

GitHub API

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

Domus Workflows

Read content from terminal (meeting-ready)

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

Search and discovery

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

Tracker aging — calculate days from origin

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

Encrypted data access (d001)

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

d000 study builds

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

Available themes

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

ISE & Network Ops

ISE ERS API — endpoint CRUD

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

Certificate inspection

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

Network diagnostics

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

ISE eval rotation — backup & restore

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

# Verify NAS repo
show repository nas-01

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

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

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

VyOS — VRRP & VLAN inspection

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

CUPS printing — validation & setup

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

PowerShell (from zsh)

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

Process management

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

Export to JSON (pipe to jq)

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

Wi-Fi management (netsh)

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

SSH from PowerShell

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

WSL ↔ Windows — Cross-Environment Commands

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

# Open Explorer to current WSL directory
explorer.exe .

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

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

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

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

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

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

Security & Encryption

View encrypted files without writing to disk

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

Batch re-encrypt — brace expansion + loop

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

Detect stale plaintext — files needing re-encryption

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

Secure delete — shred for sensitive plaintext

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

Pre-push audit — find all unencrypted project files

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

System & Infrastructure

PipeWire audio validation

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

gopass — personal document management

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

Makefile — daily workflow

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

KVM — VM & ISO management

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

Per-project file dashboard

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

USB-C / Thunderbolt Charging Diagnostics

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

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

pacman — package inspection

Source: 2026-06-25 — curl error 77 investigation, checking for package upgrades

# Check package version and install date (awk field filter)
pacman -Qi curl | awk '/^Version|^Install Date/'