Incidents and assessments
Every incident report and security assessment in the lab's Security folder, each with what happened, the root cause, the fix and the lesson I took from it.
These are the 14 incident-format reports and 4 assessments in the lab’s Security/ folder, as indexed on 2026-09-27 and 2026-09-28. One incident report is an authorized exercise rather than a real event. Each summary follows the same shape: what happened, the root cause, the fix, and the lesson. The process that produces the reports is on the incident response page, and each section links to the full report in the repository.
Index
| Date | Incident | Service | Severity | Status |
|---|---|---|---|---|
| 2026-04-19 | Credential rotation after a vendor bulletin | Vercel | Not assigned | Complete, one follow-up open |
| 2026-04-24 | TeamSpeak DNS and ServerQuery | TeamSpeak | SEV-3 | Resolved / Monitoring |
| 2026-04-24 | TeamSpeak public voice outage | TeamSpeak | SEV-2 | Mitigated |
| 2026-07-20 | HA local-storage stranding | Galaxy cluster | SEV-2 | Resolved |
| 2026-07-22 | Grafana plaintext administrator credential | Grafana | SEV-4 | Closed |
| 2026-07-22 | qBittorrent Arr client outage | qBittorrent | SEV-3 | Closed |
| 2026-07-25 | LAN-exposed repository root | Preview server | SEV-3 | Closed |
| 2026-07-30 | Blue-server duplicate volume group | Galaxy cluster | SEV-2 | Resolved |
| 2026-08-29 | Splunk administrator credential printed to a terminal session | Splunk | SEV-5 | Closed |
| 2026-09-11 | Manager processes terminated by hash refresh | Wazuh | Not assigned | Closed |
| 2026-09-12 | Action1 remote service control alert | UniFi | Not assigned | Closed |
| 2026-09-18 | ObiPC wiped from the recovery menu | Active Directory | SEV-4 | Closed |
| 2026-09-25 | Scheduled update stranded the proxy | Nginx Proxy Manager | SEV-2 | Resolved |
| 2026-09-27 | ObiPC physical access simulation | Active Directory | Informational | Exercise closed, follow-ups open |
| Date | Assessment | Result |
|---|---|---|
| 2026-07-27 | UniFi firewall audit | 62 of 100, needs_attention |
| 2026-09-23 | ObiPC event review | Operational errors and expected application-control blocks, no new incident |
| 2026-09-24 | ObiPC sign-in review | Workstation unreachable; controllers logged only its computer account |
| 2026-09-28 | ObiPC installed games review | Four unapproved games found and removed, plus an AppLocker gap through Steam’s folder |
One more incident, a Kasm Workspaces thin pool exhaustion on 2026-07-29, is archived with its retired platform and covered on the Kasm Workspaces page instead.
Incidents
Credential rotation after a vendor bulletin
2026-04-19 · Vercel · Severity not assigned · Complete with one follow-up open
What happened. In April 2026 Vercel published a bulletin about unauthorized access to some of its internal systems. My AlphaSec web application is hosted on Vercel, uses Supabase as its backend and deploys automatically from GitHub. Vercel did not notify me as one of the affected customers, but I treated the bulletin as the trigger for a precautionary rotation on 2026-04-19.
Root cause. The cause sat with the vendor. Nothing on my project was implicated, and I found no evidence of unauthorized access, so every action was precautionary.
Fix. I rotated the Supabase publishable and secret keys and deleted the old ones, rotated the database password, and disabled the legacy anon JWT key, which had still been active. I updated the account-deletion edge function’s stored secrets and the Vercel environment variable to the new keys, then uninstalled the Vercel GitHub App and reinstalled it with access to one repository only. Validation covered the key list (only the new key active, the legacy key disabled), a browser test of login, data loading and account deletion, the edge function’s logs, and a repository search that found zero rotated values. Git history was not rewritten, because the old values are no longer accepted, and active user sessions were not invalidated, because that would have signed out every user without evidence they were affected.
Lesson. A precautionary rotation is also the moment to close legacy access paths. The follow-ups include marking any future secret environment variable in Vercel as Sensitive so it cannot be read back, and setting a rotation schedule. The precautionary rotation of the Supabase personal access token had no completion record as of 2026-09-25.
TeamSpeak DNS and ServerQuery
2026-04-24 · TeamSpeak · SEV-3, service degradation · Resolved / Monitoring
What happened. Some users got “failed to connect to server” when joining the community TeamSpeak server, while the server process, its containers and the service that publishes the voice servers were all online. Separately, the TS3 Manager web administration tool kept tripping TeamSpeak’s ServerQuery flood protection and had crashed earlier.
Root cause. The public SRV record for the voice service pointed at a hostname that was itself a CNAME. SRV targets should resolve directly to the service host; some clients and resolvers tolerate an alias and others fail. The flood protection (10 commands in 3 seconds, then a 600-second ban) was hitting TS3 Manager because its ServerQuery traffic arrived from the Docker bridge gateway address, which was not on the ServerQuery allowlist. TS3 Manager had also been used with invalid connection modes, including an SSH-like mode and the public hostname. I ruled out a TeamSpeak process failure, the virtual server being offline, port publishing, the publishing service, DNS proxying, slot exhaustion and a host outage.
Fix. I pointed the SRV record directly at the public endpoint’s hostname, so users still connect with the same name, and confirmed public resolvers returned the corrected target. I added the Docker bridge gateway to the ServerQuery allowlist, confirmed TeamSpeak reloaded it, restarted only the TS3 Manager container, and a ServerQuery burst then returned error id=0 msg=ok with no flood error. TS3 Manager now uses the internal ServerQuery endpoint only; the public endpoint carries voice traffic and nothing else.
Lesson. Healthy containers and an online server do not prove the connection path a client uses; the DNS chain is part of the service. Management tools belong on internal endpoints. The report stays at Resolved / Monitoring because the external-user confirmation it asked for was never recorded. By 2026-09-25 the first server no longer existed, and each current server’s public SRV record targets the public endpoint directly.
TeamSpeak public voice outage
2026-04-24 · TeamSpeak · SEV-2 · Mitigated
What happened. At 22:49 EDT users reported that they could not join either TeamSpeak server through the public names. Basic health checks showed TeamSpeak, Docker, DNS and the publishing service all nominal, with both public endpoints registered and no bans or flood indicators. Deeper testing showed public UDP attempts changing TeamSpeak’s network counters, but the client handshake never completed.
Root cause. The most likely cause was a UDP compatibility problem across the whole path: client, then the public path, then host, then Docker’s UDP port proxy and bridge networking, then the container. Traffic reached the server path, but the handshake failed while the containers used Docker’s UDP port publishing.
Fix. I restarted the publishing service to clear stale session state, then moved both TeamSpeak containers to host networking and gave the second server its own native voice port. I verified all six listeners, updated the ServerQuery allowlists for TS3 Manager’s source traffic and updated the deployment record. At 23:04 EDT, 15 minutes after the report, TeamSpeak logged successful client connections through the public path. I found no sign of credential compromise or unauthorized administration, so I classified it as an availability incident.
Lesson. Container uptime and open listeners did not prove the handshake worked, and client connection logs were what separated an incomplete handshake from an application rejection. New voice servers go on host networking: by 2026-09-25 both servers, TeamSpeak 3 Server 3.13.8, ran that way. The report stays at Mitigated because the external-user confirmations were never recorded.
HA local-storage stranding
2026-07-20 · Galaxy cluster · SEV-2, service outage · Resolved
What happened. I shut blue-server down at 16:30 EDT as part of moving four nodes onto a UPS. Two HA-managed containers on it went down and the HA manager could not bring either back: CT 107 docker-network, which runs the NetBird control plane and Nginx Proxy Manager, and CT 108 docker-blue, which runs the RustDesk relay. Both were down for about 33 minutes, until 17:03. The HA manager moved both configurations to purple-server, but their disks stayed on blue, so every start attempt failed with no such logical volume and HA parked both in the error state. My attempts to migrate them back returned exit 255 with service 'ct:107' in error state, must be disabled and fixed first.
Root cause. Both containers kept their root disks on local-lvm, which is node-local LVM-thin storage, and the cluster has no shared storage that blue, purple and red can all reach. An HA relocation can move the configuration but never the disk. I had accepted that exposure when I built docker-network. The trigger was the HA shutdown policy: with no setting in datacenter.cfg it defaults to conditional, under which a reboot freezes a node’s HA services but a shutdown relocates them. This was a real power-down, so they moved. Data was never at risk: the containers stopped cleanly, never ran anywhere else, and their disks were intact.
Fix. I removed both from HA to clear the error state, moved each configuration file back to blue’s node directory, started them, and verified NetBird, Nginx Proxy Manager and both RustDesk services. Then I re-added them to HA and pinned them to blue with a strict node-affinity rule, so a future blue shutdown would leave them stopped until blue returned instead of stranding them. By 2026-09-24 neither container was an HA resource any more and the pin was gone, which removes this failure path entirely.
Lesson. HA on node-local storage starts and monitors a guest but does not give failover. Before powering a node down, I confirm every HA guest on it is either on shared storage or pinned to that node. And a reboot and a shutdown are different events to the HA manager.
Grafana plaintext administrator credential
2026-07-22 · Grafana · SEV-4, potential disclosure, no confirmed use · Closed
What happened. While adding Grafana’s internal HTTPS name, I found its bootstrap administrator password still sitting in plaintext in the live Compose file on security-01. Grafana only needs that value when it first initializes its database, but the Compose definition kept injecting it into every recreated container’s environment.
Root cause. I left a one-time installation input in the long-lived Compose definition after the database existed, which turned it into a persistent plaintext configuration value. The file was readable by my administrator account and root; my home directory’s permissions blocked other local users from reaching it, and Grafana had no public DNS record or port forward.
Fix. I removed the variable from the Compose file, recreated the container at 13:31:45 EDT and confirmed the variable was absent from its environment, then rotated the administrator credential so the old value no longer authenticated. Grafana logged an authenticated request at 13:46:50 and reported a healthy database. Neither the tracked repository nor the evidence held the value. Because recreating the container replaced its earlier logs, I classified this as a potential disclosure with no confirmed compromise, not as proof the old credential was never used.
Lesson. A bootstrap password does not belong in Compose after the application’s database exists. Removing it from the file is not enough: the container has to be recreated, the credential rotated, and the resulting environment inspected. Directory permissions happened to protect it, but the rule is to keep reusable credentials out of Compose rather than rely on them.
qBittorrent Arr client outage
2026-07-22 · qBittorrent · SEV-3, availability of download-client control · Closed
What happened. Sonarr and Radarr reported all download clients unavailable for about 7 hours 18 minutes, from 13:31:43 to 20:49:22 EDT, with Unable to communicate with qBittorrent via Proton VPN. Both could still reach qBittorrent over the network, but every API request returned HTTP 401 in milliseconds. Changing only the Host header to qBittorrent’s new HTTPS name returned 200. Gluetun stayed healthy and qBittorrent stayed inside its network namespace; there was no sign of public exposure or VPN bypass.
Root cause. The internal HTTPS change that morning replaced qBittorrent’s accepted WebUI domain list with its new HTTPS name alone. qBittorrent validates the Host header before it evaluates authentication, so requests addressed to the container hostname both Arr apps use were rejected outright. The change’s own verification passed because it tested only the reverse-proxy route, which sends the one allowed name.
Fix. Through qBittorrent’s API I set the domain list to three entries: the HTTPS name, the container hostname and the media host’s direct address, with Host-header validation still enabled. Both Arr apps’ saved download-client tests returned 200, both health endpoints came back clean, and the VPN’s forwarded port still matched qBittorrent’s listening port. I changed nothing in either Arr app, and I added an Arr-to-qBittorrent API probe to the HTTPS change’s verification steps.
Lesson. A test of the proxy route does not validate an application’s separate backend hostname. The regression test that would have caught this takes two seconds: call qBittorrent’s version endpoint from both Arr containers.
LAN-exposed repository root
2026-07-25 · Preview server · SEV-3, potential disclosure, no confirmed access · Closed
What happened. The first version of the small Node preview server I wrote on my workstation jedi-pc called .listen() with a port and no host, so it answered on every interface, and it served files straight from the repository root with no allow list. While it ran, a fetch of a sensitive file from elsewhere on the LAN returned it in full. Every file in the working tree was fetchable by path: 1,210 files and about 94.7 MB, including 573 files under Sensitive/. The worst of those were the complete git history from before the public repository was scrubbed and the private redaction map that ties every placeholder in the public repository back to its real value. No live service credential or private key was in the tree on that machine.
Root cause. I wrote a single-user tool and never named the interface it should listen on, so Node’s default filled in the most permissive option. Rooting it at the repository was a second, independent defect. Reading the controller’s zone matrix showed who could reach it: the six networks in the Internal zone and the five in the VPN zone, but not the internet, IoT, the DMZ or any lab or server network. The firewall behaved exactly as configured. There were no access logs, so nothing suggests anything was fetched and nothing proves it was not.
Fix. The replacement server binds to loopback explicitly, serves only an allow list (replaced on 2026-08-04 by per-request membership in git ls-files, which follows the repository’s publication boundary), rejects dotfiles, and re-checks the resolved path so .. cannot climb out. Validation showed the LAN address refusing connections and every sensitive path returning 404 on loopback. On 2026-07-27 I moved the history bundles and the redaction map out of the working tree entirely and confirmed their SHA-256 values matched after the move.
Lesson. .listen(port) binds every interface, so a tool meant for one machine has to say so in its code. The bind address and the allow list are separate controls and I needed both. And a public .gitignore is a directory of the private paths, so a local server has to assume an attacker already knows where to look.
Blue-server duplicate volume group
2026-07-30 · Galaxy cluster · SEV-2, service outage · Resolved
What happened. I added a 500 GB SATA disk to blue-server and shut it down at 23:19 EDT. When it booted at 23:31, local-lvm stayed inactive. CT 104 monitor-01 failed to autostart, and HA put CT 107 and CT 108 into error after four failed starts each. NetBird, Nginx Proxy Manager and RustDesk stayed down about 28 minutes longer than the planned shutdown, and Prometheus about 31.
Root cause. The new disk was not blank. It still held an older standalone Proxmox installation with its own LVM volume group named pve, the same default name as blue’s live installation on its NVMe drive. Proxmox’s storage configuration addresses the thin pool by volume group name, not UUID, so once LVM saw both disks the activation request became ambiguous and failed with Use --select vg_uuid=<uuid> in place of the VG name. The shutdown was only the trigger: the next boot was the first to discover the disk.
Fix. I mapped both disks, their volume groups and logical volumes, then renamed only the inactive SATA volume group by its exact UUID, which removed the collision without touching the live NVMe metadata. local-lvm came back active, I started CT 104, and I moved CT 107 and 108 through HA’s disabled state to clear their error latches before starting them. Only after confirming the SATA disk’s identity and that nothing was mounted from it did I wipe its stale LVM layout and partition table. Prometheus needed one more manual start. The disk later passed a full extended SMART read, so the failure was a name collision, not hardware.
Lesson. A newly installed disk is not blank because it is not mounted. Before booting a Proxmox node with a reused disk attached, I check lsblk, wipefs, pvs and vgs for leftover boot and LVM metadata. Renaming by UUID first restored service while keeping the destructive decision for later.
Splunk administrator credential printed to a terminal session
2026-08-29 · Splunk · SEV-5, credential disclosure into a session transcript · Closed
What happened. While building the unifi_insights app I staged the Splunk administrator credential into a mode-600 netrc file on splunk-siem, using the password manager’s inject command so the value never passed through a command line. When REST calls started returning Unauthorized, I checked which host the file was scoped to with awk '{print $1, $2, $3, $4, "password <redacted>"}'. A netrc has one key and value per line, so on the third line field two is the password, and the command printed it into the terminal session I was working in. That session kept a transcript, and the transcript was the one copy I could not fully recall. The actual fault was elsewhere: the file named the loopback address while my requests used the name localhost, so curl never offered the credentials.
Root cause. I inspected a credential file’s contents to answer a question about its scope, which needed only the first field of the first line. The compounding error was assuming print $1, $2 prints only keys.
Fix. I shredded the netrc file and the eleven scratch scripts staged beside it, confirmed the session transcript saved on disk carried a redaction marker in place of the value, and swept every readable file in my home directory and all 3,371 blobs in the repository’s history for the value, reading the comparison value from the password manager through a pipe. All sweeps found zero matches. The Splunk administrator credential was not rotated, and folding the read pattern into my credential-handling guidance is still open.
Lesson. Never read a credential file to learn something about it; read its metadata. Any print $2 over a netrc is a disclosure by construction. Do not decorate output with the word “redacted”, because a line that looks sanitized invites a reader to skim past it. And Unauthorized from a netrc is almost always the machine line, not the credential.
Manager processes terminated by hash refresh
2026-09-11 · Wazuh · Severity not assigned, loss of agent event collection · Closed
What happened. The weekly Wazuh hash-list refresh ran at 04:57:47 EDT on 2026-09-06, rebuilt 730 entries and restarted the manager. The log reported a successful restart, followed at once by SIGTERM and shutdown messages, and when the refresh unit finished at 04:58:09 no manager process remained. wazuh-manager.service still reported active. Agents could not reach the manager until 2026-09-11. I found it when app-01’s Wazuh connection showed pending during disk replacement checks, then reproduced connection timeouts to both agent ports from edge-01, while the manager host showed the unit active, neither port listening and no manager processes.
Root cause. The refresh script called wazuh-control restart directly from inside its own oneshot systemd service. The new manager processes were children of that service, in its cgroup, so systemd killed them when the service exited. wazuh-manager.service uses Type=forking with RemainAfterExit=yes, so it kept its active state and its old start time with nothing running. Disk and memory exhaustion were ruled out: root had 68 GiB free, memory about 5 GiB available, and the kernel journal showed no OOM kill.
Fix. The script now runs systemctl restart wazuh-manager.service and requires systemctl is-active --quiet before reporting success. As the regression test I ran the actual refresh service: it finished successfully, the manager stayed active, both agent ports listened, and the remote daemon’s cgroup was wazuh-manager.service. Fresh connection tests from edge-01 passed with no firewall change and no re-enrollment, and by about 3:29 AM all 16 remote agents enrolled at the time were active.
Lesson. Systemd’s active state alone did not catch a five-day outage. A process, listener or manager API health check is needed for this class of failure, and it is an open item. And a service that restarts another daemon has to do it through systemctl.
Action1 remote service control alert
2026-09-12 · UniFi · Severity not assigned, IPS block of authorized management traffic · Closed for these two flow records
What happened. At 2:47 PM a Discord message from Splunk reported that the gateway’s IPS had blocked ET RPC DCERPC SVCCTL - Remote Service Control Manager Access traffic from HQ-MGT01 on the identity network. A live UniFi flow query resolved the destination to ObiPC on the Secure Client network. There were two aggregated blocked flow records, starting at 2:03:53 PM and 2:36:07 PM.
Root cause. HQ-MGT01 runs the Action1 Deployer, which manages workstations over remote procedure calls. The UniFi IPS matched its service-control traffic to ObiPC against that signature and blocked it. An ordinary firewall allow for that path does not exempt traffic from IPS inspection. The timestamps matched the Deployer’s log of installing the agent update service on ObiPC, and ObiPC recorded the matching service-install event one second later. The Deployer process carried a valid Authenticode signature from Action1 Corporation, and the Action1 agent on ObiPC was running with automatic start. The remote operations had succeeded despite the blocks.
Fix. None, deliberately. I did not suppress the signature, exempt the host from IPS, change the firewall, stop the Deployer or silence the Splunk notification. Repeating the operations to reproduce the failure would have added a change without showing anything needed repair, so I used the existing flow and application logs as a retrospective correlation check and retained the readback with every command’s output and exit code.
Lesson. The alert can recur whenever Action1 performs the same operation. A future failed deployment needs its own timestamp and endpoint evidence before any narrowly scoped IPS exception is considered. The closure covers these two flow records, not a host-wide compromise assessment.
ObiPC wiped from the recovery menu
2026-09-18 · Active Directory · SEV-4, availability · Closed
What happened. I found ObiPC, a physical Windows 11 Pro domain workstation used by a restricted standard account, sitting at the out-of-box setup screen. That user had reset the machine to factory state from the Windows recovery menu, choosing “Remove everything” and “Clean the drive”. The reset took the domain membership, the AppLocker allowlist, the Settings lockdown, the SSH server, the Action1 agent and the applications with it. I rebuilt and rejoined the machine by about 7:10 PM, before I knew how it had happened. The directory side and the second drive were untouched, and no credential was exposed.
Root cause. A Windows 11 default I had not closed. Microsoft documents that “Reset this PC, Remove everything” from the recovery environment requires no authentication by default, while “Keep my files” does prompt. The recovery menu was reachable without signing in, because the sign-in screen offered the power button and Shift plus Restart opens it; the recovery environment was enabled; and BitLocker was off. AppLocker could not have helped, because Shift plus Restart is a shell action rather than a process launch, and two interrupted boots reach the same menu with no software involved. The restriction design from 2026-09-12 had controlled what the account could run and see inside Windows and treated the recovery environment as out of scope. Which entry point was used cannot be established, because the reset removed the previous installation.
Fix. By 11:14 PM: the recovery environment disabled, and kept disabled by a SYSTEM task at every boot and daily because feature updates re-enable it; recovery tools set to require an administrator account, through both the local MDM bridge and Group Policy; the sign-in screen power button removed and the power menu removed for the restricted group; AppLocker denies for shutdown.exe, the reset entry points, boot and recovery tools, and script and DLL hosts, by path and publisher, including the copies under WinSxS; and Settings reduced to an explicit showonly: allowlist after finding the earlier policy used an undocumented prefix. The effective policy carried 56 executable rules, 44 of them denies for the restricted group, and Test-AppLockerPolicy returned Denied for each targeted binary.
Lesson. A restricted account on a physical machine has to be restricted below Windows as well as inside it. BitLocker with a PIN is the control that would make the recovery environment harmless rather than merely hidden, and it is now the recommended baseline.
Scheduled update stranded the proxy
2026-09-25 · Nginx Proxy Manager · SEV-2, service outage · Resolved
What happened. At 3:00 AM Dockhand’s daily auto-update pulled a new Nginx Proxy Manager image (2.16.0) on docker-network and stopped the running 2.15.1 container so it could recreate it. The recreate never happened. The container exited cleanly and stayed down for about 3 hours 12 minutes, until 6:12:39 AM. Every internal HTTPS name behind the proxy refused connections, NetBird and 23 internal application hosts among them, and all six Dockhand agents lost their connection to Dockhand. The backends themselves stayed up and no data was lost.
Root cause. Dockhand updates a container by stopping it and then creating its replacement, and it sends both steps through its Hawser agent’s connection. Every Hawser agent reaches Dockhand through a proxy host on Nginx Proxy Manager, so stopping the proxy cut the connection carrying the update halfway through. unless-stopped does not restart a container that was stopped on request. Dockhand’s manual names this failure: a container Dockhand depends on to reach Docker must carry dockhand.update=false. I had applied that label to the Hawser containers on 2026-09-15 but not to the proxy, which tracked latest. The same sequence would have run again the next night.
Fix. I started the container on its existing image, copied the Compose file before editing it, pinned the image to 2.15.1 (confirming that tag resolved to the same image ID already running) and added the dockhand.update: "false" label. I applied the same change to Dockhand’s imported copy of the stack, so a Dockhand deployment cannot undo it, and updated the tracked Compose reference. Every live proxy host then returned 200 or an application redirect with a valid certificate. The proxy no longer auto-updates; 2.16.0 waits on the host for a planned manual upgrade.
Lesson. An updater must not update a component its own control path runs through. I had not yet watched a scheduled 3:00 AM run with the label in place when the report was written; the first was due on 2026-09-26.
ObiPC physical access simulation
2026-09-27 · Active Directory · Informational, authorized exercise · Closed as an exercise, follow-ups open
What happened. I tested what attempted sign-in looks like after someone gains physical access to ObiPC, then reviewed its Windows logs over a fixed 72-hour window, from 8:11:43 AM on 2026-09-24 to the same time on 2026-09-27. The window held 22 failed interactive logon events and no successful human interactive, cached-interactive, unlock or RDP logon.
| Failure reason | Count |
|---|---|
| Account disabled | 15 |
| No logon servers available | 4 |
| Account name not found | 3 |
Twenty failures used names mapped to the restricted account I had disabled on 2026-09-23; two used another alias. All were Security event 4625, logon type 2, from the local machine.
Root cause. My own authorized exercise. The rejection reasons were account disablement, unavailable logon servers and unsuccessful name resolution, and I found no authentication bypass in the selected records. The 1,416 successful logon events in the window were system, service, computer-account, management-service and investigation activity, and I separated them by logon type rather than counting every 4624 event as a person:
| Account or category | Logon type | Count |
|---|---|---|
SYSTEM | 5 | 1,253 |
SYSTEM | 0 | 6 |
| Local and network service | 5 | 12 |
| SSH service account | 5 | 1 |
| The computer account | 3 | 50 |
| The Action1 deployment account | 3 | 68 |
| My own SSH check | 3 | 2 |
| Windows virtual accounts | 2 | 24 |
Fix. A read-only investigation: a defined window, a query of the workstation, separation of human attempts from system and management logons, verification of auditing and retention, and a repeated query that matched the first. Logon auditing recorded both success and failure, the oldest retained event predated the window, and there were no log-clearing or audit-policy-change events. I made no containment, account or policy changes, and kept selected-field exports, the exact queries, exit codes and an integrity manifest. It is not a forensic acquisition.
Lesson. Counting every successful logon event as a person signing in would have produced the wrong answer; logon type is what matters. One gap remains: Other Logon/Logoff Events is not audited, so the absence of unlock and reconnect events does not prove none occurred. Follow-ups cover that auditing, validating centralized detection, and a controlled retest with recorded inputs. The closure does not certify resistance to recovery-menu abuse, external boot, offline disk access or credential theft, which were not assessed.
Assessments
UniFi firewall audit
2026-07-27 · UniFi · 62 of 100, needs_attention
Scope. A read-only audit after the zone and object consolidation: controller health, every firewall policy, zone membership, network objects, WLANs, traffic routes, switches, ports and port profiles. The controller ran UniFi Network 10.4.57 with five adopted devices online, no active alarms, 25 network objects in 14 zones, and 361 firewall policies, 59 of them custom.
| Domain | Score | Findings |
|---|---|---|
| Segmentation | 20/25 | 1 |
| Egress control | 20/25 | 3 |
| Rule hygiene | 0/25 | 31 |
| Topology | 22/25 | 3 |
| Overall | 62/100 | 38 |
Findings. Two critical: the base Management network still sits in the Internal zone, so the controller’s Internal allow lets other Internal networks reach it instead of only named admin sources; and a broad VPN allow precedes the block for a temporary VPN, which would shadow it if that VPN were enabled (it was disabled, so the conflict was latent). Three warnings: IoT traffic keeps default internet access without a restrictive egress policy, DNS interception could not be confirmed, and one DMZ block is shadowed by an equivalent earlier rule. Informational: no threat-intelligence group on an active policy, 29 custom policies with no description, and three unused switch port profiles. The score is an independent hardening rating; the consolidation’s own checks all still passed.
Follow-up. No live change was made during the review. A separate hardening change should limit the Management network to named admin sources, reorder or narrow the VPN rules, define the IoT egress and DNS-enforcement model, remove the redundant DMZ block, describe the 29 policies and remove the unused profiles.
ObiPC event review
2026-09-23 · Active Directory · No new incident established
Scope. A read-only review of ObiPC’s Security, System, Application, Defender and AppLocker logs for 2026-09-23, after its restricted account was disabled that evening.
Findings. The App Portal agent crashed around its upgrade to 0.8.0 and then logged 134 warnings, most of them containing the value 404. Group Policy logged four authentication errors with “The user name or password is incorrect”, three of them before the account was disabled. AppLocker blocked ten executables (FileCoAuth.exe, the standalone OneDrive updater and ngentask.exe) and four packaged-app deployments, with matching Windows Update errors that Microsoft defines as deployment blocked by policy. There were twenty Bluetooth driver errors. On the security side, the only failed sign-ins were the two already recorded, the queries for audit-policy changes, log clearing, account and group changes, service installs and scheduled-task changes returned nothing, and Defender reported no detections with every protection enabled.
Result. Operational errors and expected application-control blocks. The App Portal warnings, the Group Policy failures and the Bluetooth errors remain unexplained, and the absence of selected events is not treated as proof of no compromise.
ObiPC sign-in review
2026-09-24 · Active Directory · Local sign-in verification left open
Scope. A check for sign-in activity on ObiPC from midnight to about 4:43 PM. Both attempts to reach the workstation over SSH failed with host unreachable, so its local Security log could not be read. I queried both domain controllers instead for logon, Kerberos and NTLM events matching the workstation or the disabled account.
Findings. HQ-DC01 logged 381 successful network logons and HQ-DC02 33 plus two Kerberos ticket requests, all for the workstation’s computer account. No human-user authentication or matching failure appeared, and both controllers reported the restricted account disabled.
Result. Computer-account activity shows the machine working, not a person signing in, and controller logs cannot see attempts rejected locally. The open local check was resolved by the collection in the physical access simulation three days later.
ObiPC installed games review
2026-09-28 · Active Directory · Unapproved games removed; an AppLocker gap found
Scope. Which games were installed on ObiPC and how they got there, read from uninstall entries, Steam library manifests and the other launchers’ locations.
Findings. Steam had been approved and installed through App Portal on 2026-09-20. I had not approved any of the four Steam games found, about 243 GB together, which the workstation’s user installed through the Steam client starting two minutes after it landed. One of them brought Ubisoft Connect and its anti-cheat with it. The more important finding was Steam’s folder: it grants the local Users group Full Control, set explicitly, and the AppLocker rule for Program Files allows everything under it, so any executable copied into that folder would run. It is the same shape as an earlier carve-out the 2026-09-18 lockdown closed. I found no sign the gap had been used: all 26 executables outside the game folders were validly signed by Valve, and each game’s root executables by its publisher.
Result. I removed the four games and later Steam, Ubisoft Connect, its anti-cheat and a Grand Theft Auto V Enhanced remnant, using a one-off SYSTEM task instead of the vendor uninstallers so a silent restart could not interrupt the open session. The task deleted the services, a scheduled task, 26 firewall rules, folders, shortcuts and registry keys, and a verification pass found none of them left. Removing Steam also removed the writable folder, but Steam is still in the App Portal catalog, which installs without my approval, so the gap can come back. The fix is tracked in the Active Directory backlog.