CR1MS0N Security — Offensive Security Infrastructure
Built and operated by CR1MS0N-Operator | CR1MS0N Security™
Veil is the operational infrastructure layer connecting all CR1MS0N Security nodes. It is not a simulation environment — it is a production-grade offensive security homelab built for real adversary emulation, threat detection, and red team infrastructure research.
| Node | Role | OS | WireGuard IP | LAN IP |
|---|---|---|---|---|
| Cerberus | Edge node — services, detection, honeypot | Arch Linux (headless) | 10.10.10.1 (hub) |
10.10.10.251 (static) |
| NightForge | Operator workstation — tooling, compute, development | Arch Linux + Niri WM | 10.10.10.3 |
10.10.10.156 (DHCP) |
| Tairn | Attack node — Mythic C2, agent staging, lab targets | NixOS 24.11 (declarative) | 10.10.10.4 |
10.10.10.230 (libvirt NAT) |
| Hermes | Redirector VM — C2 egress, traffic forwarding, disposable | Alpine Linux 3.23.3 | 10.10.10.5 |
10.10.10.200 (libvirt NAT) |
| iPhone | Mobile WireGuard client | iOS | 10.10.10.2 |
— |
All inter-node communication runs exclusively over a WireGuard hub-and-spoke mesh. No node is directly reachable from WAN.
Internet
│
┌──────▼──────┐
│ Cerberus │
│ 10.10.10.1 │
│ 10.10.10.x │
│ │
│ Cowrie │
│ Suricata │
│ Pi-hole │
│ Caddy TLS │
│ Gitea │
│ Vaultwarden │
│ Netdata │
│ Homepage │
└──────┬──────┘
│ WireGuard mesh (10.10.10.0/24)
│
┌────────────┼────────────────┐
│ │ │
│ 10.10.10.3 │ │ │ 10.10.10.4 │
│ 10.10.10.x │ │ │ 10.10.10.x │
│ │ │ │ (libvirt NAT, │
│ Operator WS │ │ │ NightForge-local) │
│ Niri WM │ │ │ │
│ Offensive tooling │ │ │ Mythic C2 │
│ Podman profiles │ │ │ Poseidon agent │
│ Neovim / tmux │ │ │ HTTP C2 profile │
│ Ollama (local AI) │ │ │ Lab target VMs │
│ │ │ │ NixOS declarative │
│ libvirt host │ │ └────────────────────────┘
│ ┌────────────────┴──┴────┐
│ │ Hermes │
│ │ 10.10.10.5 │
│ │ 10.10.10.200 │
│ │ (libvirt NAT, │
│ │ NightForge-local) │
│ │ │
│ │ Alpine Linux │
│ │ Nginx redirector │
│ │ C2 egress proxy │
│ │ WireGuard only access │
│ │ Disposable — rebuild │
│ │ from Terraform+XML │
│ └────────────────────────┘
│ │
└───────────────────┘
WireGuard topology: Hub-and-spoke via Cerberus. Cerberus is the always-on edge node and mesh hub. NightForge, Tairn, and Hermes peer exclusively through Cerberus. Hairpin routing enabled for node-to-node communication across the mesh. NightForge also hosts two VMs (Tairn, Hermes) under libvirt NAT (10.10.10.0/24), accessible via WireGuard IPs on the mesh.
The Veil network spans two physical subnets bridged by WireGuard:
Internet ─── Home Router ─── LAN (10.10.10.0/24)
│
┌──────────────┴──────────────┐
│ │
Cerberus (10.10.10.251) NightForge (10.10.10.156)
Edge node, always-on Operator workstation
NightForge
┌──────────────────┐
│ virbr0 bridge │
│ 10.10.11.1 │
└──┬───────────┬───┘
│ │
Tairn VM Hermes VM
10.10.11.230 10.10.11.200
NixOS Alpine
Cerberus (10.10.10.1) ─── hub, always-on
│ │ │
│ │ └── Hermes (10.10.10.5) — C2 redirector
│ │
│ └──────── Tairn (10.10.10.4) — Mythic C2
│
└────────────── NightForge (10.10.10.3) — operator workstation
iPhone (10.10.10.2) — mobile client (connects to Cerberus only)
- All spoke-to-spoke traffic routes through Cerberus (hairpin forwarding)
- Tairn initiates to Cerberus only — no endpoint configured on Cerberus for Tairn
- Cerberus cannot reach
10.10.11.x— libvirt NAT is NightForge-local only - Hermes and Tairn are WireGuard-only accessible; no direct LAN path
- NightForge SSH config uses WireGuard IPs (
10.10.10.4,10.10.10.5), not libvirt NAT IPs
How traffic routes through the Veil mesh from each node's perspective.
10.10.10.3 10.10.10.1 10.10.10.4 │ nftables forward: iif "wg0" oif "wg0" accept rp_filter=0 on wg0
The Mythic C2 web UI (`https://10.10.10.4:7443`) is accessed exclusively over WireGuard. Tairn's DOCKER-USER iptables chain restricts port 7443 to `10.10.10.0/24`.
### C2 Agent Callback (Internet → Hermes → Tairn)
WAN 10.10.10.1 10.10.11.1
port forward │
Hermes (10.10.10.5)
Nginx reverse proxy
│
Tairn (10.10.10.4)
Mythic C2 (7443)
Hermes acts as the C2 redirector. Nginx on Hermes terminates the external-facing TLS and forwards traffic to Mythic on Tairn. Hermes is disposable — rebuilt from Terraform + libvirt XML on demand. This provides an operational security layer: the redirector is burned and replaced at configurable intervals or after compromise indicators.
WAN ─── Cerberus (enp0s21f0u2c2)
│
├── Suricata (af-packet, all traffic) → fast.log → Shield scoring → nftables blackhole
│
└── Cowrie (port 22, fake SSH) → cowrie.json → Shield scoring → nftables blackhole
Shield scoring:
suricata_exploit → +3 | cowrie_login → +4
suricata_c2 → +5 | cowrie_command → +3
suricata_scan → +1 | cowrie_download → +5
suricata_alert → +2 | Block threshold: ≥ 4 → 1hr blackhole
The Shield scoring engine reads Suricata alerts and Cowrie events simultaneously. Any source IP exceeding a cumulative score of 4 is added to the nftables blackhole set with a 1-hour TTL. Blocked IPs are also enqueued to /var/nightforge/scan-queue.txt for optional follow-up Nuclei recon.
NightForge ──wg0── Cerberus:8489 (Caddy reverse proxy)
│
┌────────┴────────┐
│ │
git.lan:8486 dash.lan:8282
gitea.lan:3000 vault.lan:8081
search.lan:8888 pihole.lan:8083
All .lan services are served over TLS via Caddy local CA. NightForge has the CA root certificate installed. WireGuard hairpin routing plus Pi-hole .lan DNS resolution make services available at their domain names from any mesh node.
Chromebook running headless Arch Linux. Serves as the perimeter sensor platform and homelab services hub. All services run as rootless Podman Quadlets under systemd.
Detection stack:
- Cowrie 2.9.13 — SSH honeypot on port 22. Captures attacker TTPs, credentials, and session data.
- Suricata 8.0.3 — Network IDS with live rule updates.
- Pi-hole — DNS sinkhole +
.landomain resolution for all nodes.
Services:
- Gitea — Self-hosted Git server (primary remote for all Veil repos)
- Vaultwarden — Self-hosted password manager
- Caddy — Reverse proxy with automatic local CA TLS for all
.landomains - Netdata — Real-time performance monitoring
- Homepage — NOC dashboard
Firewall: nftables. WireGuard hairpin routing via iif "wg0" oif "wg0" accept. rp_filter=0 on wg0 interface. Dynamic blackhole set for blocked IPs (1hr TTL).
Primary operator environment. Arch Linux with Niri Wayland compositor. All offensive tooling, development, and infrastructure management runs here.
- Hardware: i3-10105F, 32GB RAM, GTX 1650
- Compositor: Niri (Wayland, scrolling tiling layout)
- Container runtime: Podman rootless (ad, re, web, toolbox profiles)
- Editor: Neovim with LSP
- Shell: Zsh + Starship
- VM management: libvirt (Tairn, Hermes hosted here as NAT VMs)
- Local AI: Ollama (
qwen2.5:14bfor RAG pipeline) - Dashboard: beszel agent (CR1MS0N-ops-dashboard) reporting to Cerberus on port 8489
See nightforge for full workstation configuration and operator framework.
NixOS 24.11 VM hosted on NightForge via libvirt NAT. Declarative configuration — entire system state is version controlled in configuration.nix. Dedicated to offensive operations and course lab work.
- C2 framework: Mythic (Docker-based)
- Primary agent: Poseidon (Go, Linux)
- C2 profile: HTTP C2
- Operation: Operation CR1MS0N
- Access: WireGuard-only (
10.10.10.4). Mythic UI locked to mesh via DOCKER-USER iptables chain - Courses: Certified Red Team Analyst (CRTA), Certified Red Team Infrastructure Developer (CRT-ID) via CyberWarfare Labs — all technique work documented with MITRE ATT&CK mapping
Alpine Linux 3.23.3 VM hosted on NightForge via libvirt NAT. Serves as the C2 egress redirector and traffic forwarding layer.
- Role: C2 redirector — terminates external-facing TLS, proxies to Mythic on Tairn
- OS: Alpine Linux (minimal, ~150MB footprint)
- Proxy: Nginx reverse proxy (TLS termination + stream forwarding)
- Networking: Nginx listens on port 443, upstream to
10.10.10.4:7443 - Disposable: Full VM state captured in libvirt XML + Terraform config; rebuild in <2 minutes
- Lifecycle: Static config in
/etc/nginx/nginx.conf,/etc/wireguard/wg0.conf,/etc/network/interfaces - Access: WireGuard-only (
10.10.10.5). No direct LAN path. - Monitoring: Netdata agent reporting to Cerberus
| Control | Implementation |
|---|---|
| Network segmentation | WireGuard mesh — no node directly reachable from WAN |
| C2 access control | iptables DOCKER-USER — port 7443 restricted to 10.10.10.0/24 |
| C2 redirector | Hermes — disposable Alpine Nginx proxy between WAN and Mythic |
| Traffic flow isolation | Hub-and-spoke mesh; all spoke-to-spoke traffic routes through Cerberus |
| Container isolation | Rootless Podman Quadlets (Cerberus), Docker (Tairn — Mythic requirement) |
| VM isolation | Air-gapped libvirt network (virbr1, 10.10.10.0/24) for AD labs |
| Secret management | Vaultwarden + /etc/containers/secrets/ for Quadlet env injection |
| DNS security | Pi-hole (.lan resolution + ad/tracker sinkhole) |
| TLS | Caddy local CA (all .lan services), Nginx self-signed (Hermes redirector) |
| SSH hardening | Key-only auth, no root password login, port 22 is Cowrie honeypot |
| Intrusion detection | Suricata 8.0.3 (network IDS), Cowrie 2.9.13 (SSH honeypot) |
| Automated blocking | NightForge Shield — scores threats, blackholes IPs for 1hr via nftables |
| Declarative infra | Tairn entire OS state in configuration.nix — reproducible from scratch |
| Firewall default-deny | nftables input policy DROP; explicit allow rules only |
| Audit logging | auditd on Cerberus, Suricata EVE JSON, Cowrie JSON session logs |
| Remote access | Tailscale (authenticated overlay), no port forwarding |
| libvirt backend | nftables — consistent with host firewall, avoids iptables/nftables mismatch |
| Mesh partitioning | Tairn and NightForge have no direct WireGuard endpoint — Cerberus learns them dynamically |
The primary operational dashboard runs on Cerberus at dash.lan:8282. It provides real-time visibility into:
- Threat intel: Suricata alert counts, Cowrie session stats, Shield blackhole drops
- Node metrics: CPU, memory for Cerberus (via Netdata), Tairn, and Hermes (via beszel agents)
- Network health: WireGuard peer status with handshake timestamps; stale peers flagged after 300s
- Service links: Gitea, Vaultwarden, Pi-hole, SearXNG with live widgets
NOC data is sourced from:
~/noc-status/*.json— refreshed every 5 minutes by systemd timernoc-update.timer- NOC scripts in
~/scripts/noc/:wg-status.sh,cowrie-status.sh,suricata-status.sh,nftables-status.sh - Data served by Caddy on port 8484 (
/noc-status/file server)
| Service | Port | Endpoint |
|---|---|---|
| CR1MS0N-ops-dashboard | 9090 | 10.10.10.3:9090 via Cerberus Caddy proxy :8489 |
This provides:
- Multi-node system metrics (CPU, RAM, disk, uptime)
- Agent status (beszel agents on Cerberus, NightForge, Tairn, Hermes)
- Historical performance graphs
All scripts in edge-node/scripts/noc/:
| Script | Output | Description |
|---|---|---|
wg-status.sh |
wg.json |
WireGuard peer health — handshake time, status per peer |
cowrie-status.sh |
cowrie.json |
Honeypot session stats — connections, unique IPs, top attacking IP |
suricata-status.sh |
suricata.json |
IDS alert stats — high/critical alerts, persistent sources |
nftables-status.sh |
nftables.json |
Firewall stats — blackhole drops, input drops |
noc-update.sh |
all of above | Orchestrator — runs all status scripts, writes to ~/noc-status/ |
Veil uses a hybrid deployment model combining declarative configuration, infrastructure-as-code, and manual setup for constrained hardware.
| Node | Method | Tooling | Provisioning |
|---|---|---|---|
| Cerberus | Manual + Podman Quadlets | systemd, nftables, Caddy | Chromebook (ARM) — limited IaC support |
| NightForge | Manual + dotfiles | Niri, Podman, Neovim, Zsh | Desktop, see nightforge |
| Tairn | Declarative | NixOS configuration.nix |
nixos-rebuild switch — full OS state in VCS |
| Hermes | IaC | Terraform + libvirt XML | terraform apply + Alpine install |
The Hermes redirector VM is the only node with full IaC coverage:
veil/
├── terraform/
│ ├── main.tf # libvirt provider config, VM definition
│ ├── variables.tf # CPU, RAM, disk sizing
│ └── outputs.tf # VM IP, SSH config
├── ansible/
│ ├── playbooks/
│ │ └── hermes.yml # Alpine setup: WireGuard, Nginx, Netdata
│ └── roles/
│ └── hermes/
│ ├── tasks/
│ │ ├── main.yml
│ │ ├── wireguard.yml
│ │ └── nginx.yml
│ └── templates/
│ ├── wg0.conf.j2
│ └── nginx.conf.j2
└── configs/
└── libvirt/
├── tairn.xml
└── hermes.xml
Deployment sequence:
# 1. Provision the VM
cd veil/terraform
terraform init
terraform plan
terraform apply
# 2. Install Alpine (manual ISO mount + setup-alpine via virt-viewer)
# 3. Configure the redirector
cd veil/ansible
ansible-playbook playbooks/hermes.yml -i inventories/hermes.ini# Apply configuration changes
ssh tairn # 10.10.10.4 via WireGuard
sudo nixos-rebuild switch --show-trace
# Config lives at /etc/nixos/configuration.nix
# Committed in veil/configs/nixos/# See Quickstart section below
# Quadlet files in edge-node/containers/
# nftables config in configs/nftables/cerberus.nft 1. terraform apply → VM provisioned on NightForge libvirt
2. Alpine setup-alpine → OS installed
3. ansible-playbook → Nginx + WireGuard + Netdata configured
4. wg-quick up → Connects to Veil mesh (10.10.10.5)
5. ── active ── → Redirecting C2 traffic for Tairn
6. terraform destroy → VM destroyed, redirector burned
7. Return to step 1 → New VM, new SSH keys, new WireGuard keys
This burn-and-rebuild cycle is an operational security control: even if the redirector is compromised, it contains no persistent secrets and can be replaced from scratch in under 5 minutes.
veil/
├── README.md
├── configs/
│ ├── sysctl/
│ │ └── 99-wireguard.conf # Kernel tuning for WireGuard hairpin
│ └── nftables/
│ └── cerberus.nft # nftables ruleset
├── edge-node/
│ ├── containers/ # Podman Quadlet unit files
│ ├── systemd/ # User systemd units
│ └── scripts/ # Shield + NOC automation scripts
├── dotfiles/ # Shared shell/prompt configs (Starship)
├── terraform/ # Hermes redirector IaC
├── ansible/ # Hermes redirector Ansible playbooks
└── docs/
├── architecture.md
├── wireguard-mesh.md
├── cerberus-setup.md
├── tairn-setup.md
├── services.md # Full service reference
├── ops.md # Operations runbook
├── troubleshooting.md # Issue catalog
├── known-gaps.md # Active and resolved gaps
└── infra.md # Node registry, key paths, port map
# 1. Apply sysctl settings
sudo cp configs/sysctl/99-wireguard.conf /etc/sysctl.d/
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
# 2. Create data directories
sudo mkdir -p /var/nightforge/{cowrie-logs,cowrie-lib,scan-queue}
sudo chown -R $USER:$USER /var/nightforge/
sudo chown -R 100998:100998 /var/nightforge/cowrie-logs /var/nightforge/cowrie-lib
# 3. Deploy Cowrie honeypot
cp edge-node/containers/nightforge-cowrie.container ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start nightforge-cowrie.service
# 4. Enable linger
loginctl enable-linger $USERCommon operational tasks for maintaining the Veil mesh and Cerberus edge node.
# Overall node health
systemctl --user status nightforge-cowrie nightforge-shield
sudo systemctl status suricata container-pihole container-gitea container-vaultwarden
# WireGuard mesh health
sudo wg show
sudo nft list set inet filter blackhole
# Scan queue depth
wc -l /var/nightforge/scan-queue.txt
tail -5 /var/nightforge/scan-queue.txt# NightForge Shield (scoring engine)
systemctl --user restart nightforge-shield
journalctl --user -u nightforge-shield -f --output=cat
# Cowrie honeypot
systemctl --user restart nightforge-cowrie
journalctl --user -u nightforge-cowrie -n 50
# Suricata IDS
sudo systemctl restart suricata
sudo journalctl -u suricata -n 30
# Manual services (until Quadlet migration complete)
podman restart homepage
podman restart vaultwarden# View mesh status
sudo wg show
# Add new peer to mesh
# 1. Generate keys on new node: wg genkey | tee privatekey | wg pubkey > publickey
# 2. Add peer to Cerberus wg0.conf:
# [Peer]
# PublicKey = <new-node-pubkey>
# AllowedIPs = 10.10.10.x/32
# 3. Restart WireGuard on Cerberus: sudo systemctl restart wg-quick@wg0
# 4. Configure new node to peer with Cerberus
# Force re-handshake (troubleshoot stale peer)
sudo wg set wg0 peer <pubkey> endpoint <ip>:<port>
# Reload after config change
sudo systemctl restart wg-quick@wg0# View full ruleset
sudo nft list ruleset
# View blackhole set (currently blocked IPs)
sudo nft list set inet filter blackhole
# Manually block an IP (1 hour)
sudo nft add element inet filter blackhole '{ 1.2.3.4 timeout 1h }'
# Manually unblock an IP
sudo nft delete element inet filter blackhole '{ 1.2.3.4 }'
# Reload nftables from config
sudo systemctl restart nftables# View NOC status files
cat ~/noc-status/wg.json
cat ~/noc-status/cowrie.json
cat ~/noc-status/suricata.json
cat ~/noc-status/nftables.json
# Force NOC update
systemctl --user start noc-update.service
# NOC update timer runs every 5 minutes
systemctl --user status noc-update.timer
# Watch WireGuard peer health
watch -n 5 sudo wg show# Automated: daily at 02:00 UTC via cron
# Backups Vaultwarden SQLite → ~/backups/vaultwarden/ → Gitea repo
# Manual backup
bash ~/backup-script.sh
cat ~/backup.log
# Verify backup repo latest commit
curl -s http://git.lan/foreverlx/backups | grep "commit"# Weekly automated maintenance (Sunday 03:00 UTC)
# - pacman -Syu (full system update)
# - Logs to /var/log/security-updates.log
# Manual maintenance
bash ~/scripts/maintenance.sh
# - Suricata rule update
# - Log rotation (removes cowrie.json entries >3 days)
# - Vaultwarden SQLite backup
# - Homepage config backup
# - Arch package cache pre-downloadAfter a Cerberus reboot, verify the following in order:
# 1. Core services
systemctl --user status nightforge-cowrie nightforge-shield
sudo systemctl status suricata container-pihole container-gitea container-vaultwarden
# 2. Manual starts (until Quadlet migration)
podman start homepage
podman start vaultwarden
# 3. Firewall operational
sudo nft list ruleset | grep policy
sudo nft list set inet filter blackhole
# 4. Cowrie on port 22
ss -tlnp | grep :22
# 5. WireGuard mesh
sudo wg show# Cowrie not logging
ls -la /var/nightforge/cowrie-logs/ # should be owned by UID 100998
sudo chown -R 100998:100998 /var/nightforge/cowrie-logs /var/nightforge/cowrie-lib
# Shield not blocking
systemctl --user status nightforge-shield
sudo nft list sets | grep blackhole
# Vaultwarden not accessible
podman ps | grep vault
ss -tlnp | grep 8081
sudo nft list ruleset | grep 8081
# User Quadlets missing after reboot
systemctl --user daemon-reloadSee docs/troubleshooting.md for the complete issue catalog with root causes and resolutions.
Active gaps tracked in the Veil infrastructure. See docs/known-gaps.md for full details including resolved gaps.
| Gap | Severity | Phase | Effort |
|---|---|---|---|
| Netdata not monitoring containers (Podman socket not mounted) | Medium | D1 | Low |
| NightForge not integrated into centralized monitoring | Medium | N1 | Medium |
| Homepage Netdata service widgets show identical data | Low | D1 | Low |
| SearXNG autocomplete not working | Low | D1 | Low |
| Scan queue has no worker consuming the queue | Low | 4 | High |
| No network segmentation (VLANs) — flat 10.10.20.0/24 | Enhancement | 6 | Very High |
Gap 1 — Homepage Netdata Service Widgets Show Identical Data
Suricata, Cowrie, and Netdata cards all display the same Netdata CPU chart. Fix: assign distinct metrics — Suricata → suricata.alerts, Cowrie → container cgroup chart, Netdata → system.cpu.
Gap 2 — SearXNG Autocomplete Not Working
Homepage search suggestionUrl points to SearXNG autocompleter but suggestions do not appear. Likely CORS or endpoint format issue.
Gap 3 — Netdata Not Monitoring Containers
Netdata sees host metrics but container-level metrics are invisible. Podman socket not mounted into the Netdata container. Fix: add Volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro to the Netdata Quadlet.
Gap 4 — No Scan Queue Worker
Blocked IPs accumulate in the scan queue but no worker performs Nuclei recon. Planned: nightforge-recon.timer every 5 minutes running recon-worker.sh.
Gap 5 — NightForge Not Integrated into Centralized Monitoring NightForge is connected to the mesh (10.10.10.3) but not reporting to the NOC dashboard. Planned: Netdata agent, Homepage service cards, centralized log aggregation.
Gap 6 — No Network Segmentation (VLANs) Network is flat 10.10.20.0/24. Proposed zones: DMZ (honeypots), Ops (services), Red/C2 (NightForge, Mythic), Management (OOB). Prerequisites: managed switch, pfSense/OPNsense router.
The following gaps were closed during Phase S1 (Service Cleanup):
- Pi-hole password in plaintext → system Quadlet with secrets file
- Vaultwarden competing services → single user Quadlet on port 8081
- No TLS frontend → Caddy deployed with local CA
- Homepage no auto-start → user Quadlet + linger enabled
- WireGuard reboot persistence → wg-quick@wg0 enabled
- WireGuard mesh incomplete → Tairn added, hairpin routing resolved
- Caddy/Gitea/Vaultwarden/Pi-hole hand-written units → Quadlet migration
- NightForge DNS single point of failure → Cloudflare fallback DNS
- Cerberus hostname b-k3s → renamed to cerberus
- Tairn/Mythic C2 deployment → NixOS + Mythic + Poseidon deployed
- Subnet migration → all configs updated to 10.10.20.0/24 after ISP change
All tooling is deployed on infrastructure owned and operated by the author for authorized security research and portfolio development. Authorized use only.
Author: Darrius Grate (CR1MS0N-Operator) | CR1MS0N Security™ License: MIT