{"id":51656001,"url":"https://github.com/codelassey/tpot-soc-automation","last_synced_at":"2026-07-14T09:03:36.382Z","repository":{"id":364408854,"uuid":"1255257898","full_name":"codelassey/tpot-soc-automation","owner":"codelassey","description":"Build a Full-Cycle Security Operations Pipeline around a T-Pot honeypot deployment on AWS, with tailscale-enabled local Splunk SIEM log ingestion, AI-powered log analysis and intelligence (Claude + VirusTotal MCPs), incident tracking via DFIR IRIS and Slack, Ollama-based alert triage, and automated response via OPNsense Firewall Rules","archived":false,"fork":false,"pushed_at":"2026-06-12T22:08:45.000Z","size":33602,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T00:10:32.438Z","etag":null,"topics":["ai-soc","beelzebub","blue-team","cybersecurity","elk-stack","firewall","firewall-configuration","honeypot","opnsense","security-monitoring","security-operations","splunk","suricata","tailscale","threat-intelligence","tpot","virustotal"],"latest_commit_sha":null,"homepage":"","language":"YARA","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codelassey.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T15:54:11.000Z","updated_at":"2026-06-12T22:08:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/codelassey/tpot-soc-automation","commit_stats":null,"previous_names":["codelassey/tpot-soc-automation"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/codelassey/tpot-soc-automation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codelassey%2Ftpot-soc-automation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codelassey%2Ftpot-soc-automation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codelassey%2Ftpot-soc-automation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codelassey%2Ftpot-soc-automation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codelassey","download_url":"https://codeload.github.com/codelassey/tpot-soc-automation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codelassey%2Ftpot-soc-automation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35453848,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-14T02:00:06.603Z","response_time":114,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-soc","beelzebub","blue-team","cybersecurity","elk-stack","firewall","firewall-configuration","honeypot","opnsense","security-monitoring","security-operations","splunk","suricata","tailscale","threat-intelligence","tpot","virustotal"],"created_at":"2026-07-14T09:03:35.290Z","updated_at":"2026-07-14T09:03:36.372Z","avatar_url":"https://github.com/codelassey.png","language":"YARA","funding_links":[],"categories":[],"sub_categories":[],"readme":"# End-to-End Threat Intelligence and AI-Powered SOC Pipeline for Telemetry-Driven Triage and Response\n\n\n\u003cp align=\"center\" style=\"white-space: nowrap; overflow-x: auto;\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/T--Pot-Honeypot-FF9900?style=for-the-badge\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Deployed_on-AWS-232F3E?style=for-the-badge\u0026logo=amazonaws\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Splunk-SIEM-65A637?style=for-the-badge\u0026logo=splunk\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/OPNsense-Firewall-D94F00?style=for-the-badge\u0026logo=opnsense\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Suricata-IDS%2FIPS-EF3B2D?style=for-the-badge\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/n8n-SOAR-EA4B71?style=for-the-badge\u0026logo=n8n\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/DFIR--IRIS-Incident_Response-6F42C1?style=for-the-badge\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Ollama-Local_LLM-111111?style=for-the-badge\u0026logo=ollama\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/AI-Powered_Triage-000000?style=for-the-badge\u0026logo=openai\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/VirusTotal-MCP-394EFF?style=for-the-badge\u0026logo=virustotal\u0026logoColor=white\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/IPAPI-IP_Enrichment-22C55E?style=for-the-badge\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Claude-MCP_Client-D97706?style=for-the-badge\u0026logo=anthropic\u0026logoColor=white\"\u003e\n\u003c/p\u003e\n\n\n~ Closing the loop from initial probe to containment and case resolution.\n\n---\n\n## What Makes This Different\n\nMost honeypot projects stop at installing T-Pot and collecting data from the already existing Kibana dashboard. This one doesn't.. it builds a full operational cycle around the data, from passive collection all the way through to active threat containment.\n\nHere's what this project adds on top of a standard T-Pot deployment:\n\n-   **Splunk SIEM integration over a private Tailscale mesh network:** no public Splunk port exposed and no VPN headache. The universal forwarder sends logs from the EC2 instance directly to a local Splunk instance via Tailscale IP for local log storage.\n-   **AI-powered log analysis via MCP (Model Context Protocol):** Claude AI is connected directly to Splunk using the Splunk MCP server. That means natural language queries against live honeypot logs, automated correlation, and threat enrichment.. all from a conversation.\n-   **VirusTotal enrichment on all captured malware hashes:** every payload dropped by attackers (Cowrie SFTP uploads, ADBHoney ARM binaries, Dionaea EternalBlue captures) was looked up on VirusTotal. 51 hashes total. 3 were not in VirusTotal at all.\n-   **OPNsense Firewall with Suricata IDS/IPS:** This sits on the local network in IPS mode with custom rules built directly from honeypot IOCs, literally.. the attacker data feeds the detection rules that protect the network.\n-   **An Unbound DNS sinkhole:** This blocks malicious domains identified during the observation period at the resolution layer, so no device on the network can even reach them.\n-   **A full structured threat intelligence report:** Not a screenshot dump. A proper TI report covering attack timelines, coordinated actor attribution, CVE analysis, MITRE ATT\u0026CK mapping, IOCs, and recommendations.\n-   Splunk alerts trigger a webhook into N8N SOAR, which calls Ollama to enrich every IOC via VirusTotal and IPAPI and produce a structured SOC investigation report.\n-   If the AI verdict is TRUE POSITIVE, the source IP is automatically added to an OPNsense alias and firewall rules are reloaded with no human intervention for confirmed threats. In a production environment, this would require further tuning and exception handling for cloud-hosted IP ranges to minimize false positives and avoid blocking legitimate traffic.\n-   The enriched alert is simultaneously forwarded to DFIR-IRIS as a case and to Slack as a formatted notification, so the analyst always knows what was blocked and why.\n-   Analysts can then escalate alerts to full IRIS cases, extract IOCs, add investigation notes, and close cases, all within the same pipeline.\n-   Suricata and Yara rules built based on attacker telemetry.\n-   The threat intelligence feed (IP blocklist from honeypot attacker IPs) is served over HTTP from a local Python server and consumed by OPNsense as a live URL alias, so the blocklist and firewall stay in sync automatically.\n\nThis deployment will also feed data into a companion SOC automation project (separate repo) where I will build upon this project but witth a different setting and attacker mindset.\n\n---\n\n## Table of Contents\n\n1.  [Project Overview](#1-project-overview)\n2.  [Architecture](#2-architecture)\n3.  [EC2 Instance Setup](#3-ec2-instance-setup)\n    -   3.1 [IAM User \u0026 Instance Launch](#31-iam-user--instance-launch)\n    -   3.2 [Elastic IP \u0026 SSH Access](#32-elastic-ip--ssh-access)\n    -   3.3 [User Account](#33-user-account)\n    -   3.4 [Docker Installation](#34-docker-installation)\n4.  [T-Pot Honeypot Installation](#4-t-pot-honeypot-installation)\n    -   4.1 [Installation \u0026 Configuration](#41-installation--configuration)\n    -   4.2 [Security Group Rules](#42-security-group-rules)\n    -   4.3 [Verifying the Web UI](#43-verifying-the-web-ui)\n    -   4.4 [Opening to the Internet](#44-opening-to-the-internet)\n5.  [Log Decompression \u0026 Preparation](#5-log-decompression--preparation)\n6.  [Splunk SIEM Setup](#6-splunk-siem-setup)\n    -   6.1 [Installing Splunk Enterprise](#61-installing-splunk-enterprise)\n    -   6.2 [Creating the Honeypot Index](#62-creating-the-honeypot-index)\n7.  [Tailscale Network Setup](#7-tailscale-network-setup)\n8.  [Splunk Universal Forwarder](#8-splunk-universal-forwarder)\n    -   8.1 [Installation \u0026 Boot Configuration](#81-installation--boot-configuration)\n    -   8.2 [inputs.conf Configuration](#82-inputsconf-configuration)\n    -   8.3 [Troubleshooting Ingestion](#83-troubleshooting-ingestion)\n9.  [Claude AI + Splunk MCP Integration](#9-claude-ai--splunk-mcp-integration)\n    -   9.1 [Installing Claude Desktop (Linux)](#91-installing-claude-desktop-linux)\n    -   9.2 [Splunk MCP Server Setup](#92-splunk-mcp-server-setup)\n    -   9.3 [MCP Configuration File](#93-mcp-configuration-file)\n10. [Splunk Queries Detection Engineering Reference](#10-splunk-queries-detection-engineering-reference)\n     -   10.1 [Discovery Queries](#101-discovery-queries)\n     -   10.2 [Attack Surface Queries](#102-attack-surface-queries)\n     -   10.3 [Cowrie SSH/Telnet Queries](#103-cowrie-sshtelnet-queries)\n     -   10.4 [Threat Intelligence Queries](#104-threat-intelligence-queries)\n     -   10.5 [Campaign \u0026 Attribution Queries](#105-campaign--attribution-queries)\n11.  [Kibana Dashboard Queries](#11-kibana-dashboard-queries)\n12. [OPNsense Firewall Installation](#12-opnsense-firewall-installation)\n    - 12.1 [VM Setup and OS Installation](#121-vm-setup-and-os-installation)\n    - 12.2 [Interface Configuration and Web Access](#122-interface-configuration-and-web-access)\n13. [Threat Intelligence Feed \u003e OPNsense Alias](#13-threat-intelligence-feed--opnsense-alias)\n    - 13.1 [Serving the Blocklist over HTTP](#131-serving-the-blocklist-over-http)\n    - 13.2 [Creating the Tpot_threatintel Alias](#132-creating-the-tpot_threatintel-alias)\n    - 13.3 [Firewall Rules WAN and LAN](#133-firewall-rules-wan-and-lan)\n    - 13.4 [DNS Sinkhole for Malicious Domains](#134-dns-sinkhole-for-malicious-domains)\n14. [Suricata IDS/IPS Custom Rules Deployment](#14-suricata-idsips-custom-rules-deployment)\n    - 14.1 [Enabling Suricata in IPS Mode](#141-enabling-suricata-in-ips-mode)\n    - 14.2 [Deploying Custom Rules via SFTP](#142-deploying-custom-rules-via-sftp)\n    - 14.3 [Routing Traffic Through OPNsense](#143-routing-traffic-through-opnsense)\n15. [DFIR IRIS Ticketing System Integration](#15-dfir-iris-ticketing-system-integration)\n    - 15.1 [Installation via Docker Compose](#151-installation-via-docker-compose)\n    - 15.2 [Environment Configuration](#152-environment-configuration)\n16. [N8N SOAR - Installation and Workflow Setup](#16-n8n-soar---installation-and-workflow-setup)\n    - 16.1 [Installing N8N via Docker Compose](#161-installing-n8n-via-docker-compose)\n    - 16.2 [Connecting Ollama from Windows Host](#162-connecting-ollama-from-windows-host)\n    - 16.3 [Building the Splunk Alert Trigger](#163-building-the-splunk-alert-trigger)\n    - 16.4 [AI Triage Workflow - Ollama + VirusTotal + IPAPI](#164-ai-triage-workflow---ollama--virustotal--ipapi)\n    - 16.5 [Auto-Block on True Positive Verdict](#165-auto-block-on-true-positive-verdict)\n    - 16.6 [DFIR-IRIS Case Creation and Slack Notification](#166-dfir-iris-case-creation-and-slack-notification)\n18.  [Threat Intelligence Report](#18-threat-intelligence-report)\n19.  [Key Findings Summary](#19-key-findings-summary)\n20.  [Repository Structure](#20-repository-structure)\n\n---\n\n## 1\\. Project Overview\n\n| Parameter | Value |\n| --- | --- |\n| Platform | T-Pot CE v24.x (Deutsche Telekom) |\n| Deployment | AWS EC2 - `m7i-flex.large` (2 vCPU, 8 GB RAM), 128 GB storage |\n| Region | us-east-1 (United States - N. Virginia) |\n| OS | Ubuntu |\n| Observation period | 21 days |\n| Total events captured | ~3.47 million (may have some noise) |\n| Unique attacker IPs | 20,727+ |\n| Honeypot services | 20 active sourcetypes |\n| SIEM | ELK and Splunk Enterprise (local VM, forwarded via Tailscale) |\n| AI integration | Claude AI via Splunk MCP server |\n| Malware hashes analysed | 51 (Cowrie + ADBHoney + Dionaea) |\n| Local VM | Ubuntu |\n\n---\n\n## 2. Architecture\n\n![](images/media/architecture.png)\n\n---\n\n## 3. EC2 Instance Setup\n\n### 3.1 IAM User \u0026 Instance Launch\n\nStarted by creating a dedicated IAM user rather than using the root account, a standard practice and my first real AWS project principle: never use root for operational work.\n\n![](images/media/image1.png)\n\n![](images/media/image2.png)\n\n![](images/media/image3.png)\n\n![](images/media/image4.png)\n\n![](images/media/image5.png)\n\nFor the instance itself:\n\n-   **AMI:** Ubuntu (latest LTS)\n-   **Instance type:** `m7i-flex.large` - 2 vCPU, 8 GB RAM. Would have preferred 16 GB honestly, but this was the highest I had free tier access to and it held up just fine.\n-   **Storage:** 128 GB (gp3) - T-Pot runs a full ELK stack in Docker so it eats disk fast\n-   **Key pair:** Created a new `.pem` key pair for SSH access\n\n![](images/media/image7.png)  \n![](images/media/image10.png) \n![](images/media/image8.png)\n\nFor network settings during launch, I created a new security group (`launch-wizard-1`) with a single rule allowing SSH from my IP only. The plan was to lock it down tight first and open it up for attacks only after everything was verified working.\n\n![](images/media/image9.png) \n![](images/media/image11.png)\n\n### 3.2 Elastic IP \u0026 SSH Access\n\nBefore connecting to the instance for the first time, I allocated an Elastic IP, AWS's feature for a static public IP that doesn't change when the instance restarts. Really important for a long-running honeypot so you're not chasing a new IP every time there's a reboot.\n\nSteps:\n\n1.  EC2 Console \u003e Elastic IPs \u003e Allocate Elastic IP address\n2.  Associate it to the running instance\n\n![](files/images/media/image13.png) \n![](images/media/image14.png) \n![](images/media/image15.png)\n\nThen connected via SSH:\n\n```bash\nssh -i \"myhoney.pem\" ubuntu@\u003celastic-ip\u003e\n```\n\n![](images/media/image16.png) \n![](images/media/image17.png)\n\nFirst thing after getting in:\n\n```bash\nsudo apt update \u0026\u0026 sudo apt upgrade -y\n```\n\n![](images/media/image18.png)\n\n### 3.3 User Account\n\nI created a main user `apostrophe` with sudo permissions. \n\n```bash\nsudo adduser apostrophe\nsudo usermod -aG sudo apostrophe\n```\n\nThen logged out and back in as `apostrophe` for all subsequent work.\n\n![](images/media/image19.png)\n\nThe `~/.ssh/authorized_keys` file for `apostrophe` was either missing or owned by root. The server does not have the ssh public key for the user apostrophe.\n\nTo resolve the issue, I had to add my public key to the user apostrophe. Logged in as ubuntu, I acceesed and copied the public key and logged back in as apostrophe and created the file authorized\\_keys within ~/.ssh/ and pasted the public key.  \n\n```bash\nsudo mkdir -p /home/apostrophe/.ssh\nsudo cp -r /home/ubuntu/.ssh /home/apostrophe/\nsudo chown -R apostrophe:apostrophe /home/apostrophe/.ssh\nsudo chmod 700 /home/apostrophe/.ssh\nsudo chmod 600 /home/apostrophe/.ssh/authorized_keys\n```\n\n![](images/media/image25.png) \n![](images/media/image26.png) \n![](images/media/image27.png)\n\n### 3.4 Docker Installation\n\n![](images/media/image22.png)\n\nT-Pot runs entirely in Docker, so that had to go on first. Downloaded the install script from the kitpro docker guide:\n\n```bash\ncurl -fsSL https://wiki.kitpro.us/en/articles/docker-script -o install-docker.sh\nchmod +x install-docker.sh\nsudo ./install-docker.sh\n```\n\nAfter install, log out and back in to pick up the docker group membership, then verify:\n\n```bash\ndocker --version\ndocker compose version\n```\n\n![](images/media/image30.png)\n\n---\n\n## 4\\. T-Pot Honeypot Installation\n\n![](images/media/image37.png)\n\n### 4.1 Installation \u0026 Configuration\n\nCloned the official T-Pot CE repository from Deutsche Telekom Security into `/opt/`:\n\n```bash\ncd /opt\nsudo git clone https://github.com/telekom-security/tpotce.git\ncd tpotce\nsudo ./install.sh\n```\n\n![](images/media/image38.png) \n![](images/media/image39.png)\n\nInstallation options selected:\n\n-   **Type:** Hive - the standard full install with maximum capabilities\n-   **Web UI credentials:** Set a username and password (not writing them here obviously lol)\n\n![](images/media/image40.png) \n![](images/media/image41.png)\n\nThe installer pulls all the Docker images, configures the ELK stack, and changes the main SSH port. In my case it moved to **port 64295** - the old port 22 becomes the Cowrie SSH honeypot.\n\nAfter install completed, rebooted the instance:\n\n```bash\nsudo reboot\n```\n\n![](images/media/image42.png)\n\n### 4.2 Security Group Rules\n\nBefore reconnecting, updated the EC2 security group inbound rules:\n\n| Port Range | Protocol | Source | Purpose |\n| --- | --- | --- | --- |\n| 64295 | TCP | My IP only | Admin SSH access |\n| 64297 | TCP | My IP only | T-Pot Web UI |\n| 22 | TCP | 0.0.0.0/0 | Cowrie SSH honeypot |\n\nThe logic: everything on the admin ports is locked to my IP, everything on the honeypot ports is wide open to the internet.\n\nReconnected using the new port:\n\n```bash\nssh -i \"myhoney.pem\" -p 64295 apostrophe@\u003celastic-ip\u003e\n```\n\n### 4.3 Verifying the Web UI\n\nAccessed the T-Pot web interface at `https://\u003celastic-ip\u003e:64297`. Kept getting a \"site can't be reached\" error. Everything looked fine in the backend though:\n\n```bash\nsudo systemctl status tpot\nsudo docker ps\n```\n\n![](images/media/image48.png)\n\nAll containers were running. Turned out the issue was that my IP filter in the security group rule was too restrictive - a quirk with how AWS handles source IP matching on some browser setups. Fixed it by temporarily allowing `0.0.0.0/0` on port 64297, confirmed the UI loaded, then immediately locked it back to my IP.\n\n![](images/media/image49.png)\n\nFirst time logging in I had a credential issue. The exclamation mark in my intended username (`questionmark!`) was being silently dropped by the T-Pot user management script. When I thought everything was going well... nope. Used the `genuser.sh` script from the tpot directory to create a clean new credential set:\n\n```bash\ncd /opt/tpotce\nsudo ./genuser.sh\n```\n\n![](images/media/image50.png) ![](images/media/image51.png)\n\nAfter that, the UI loaded clean. Confirmed all the built-in tools were working:\n\n-   Attack Map \n-   CyberChef\n-   Spiderfoot\n-   Kibana (was still loading - takes a couple of minutes on first boot)\n\n![](images/media/image52.png) ![](images/media/image53.png) ![](images/media/image54.png) ![](images/media/image55.png) ![](images/media/image56.png)\n\n### 4.4 Opening to the Internet\n\n![](images/media/image57.png)\n\nOnce everything was confirmed working, updated the security group to allow all inbound traffic on ports 0–64000. Rebooted the instance and within a few minutes, hits started coming in.\n\n-   First hits: within minutes of going live\n-   After 7 hours: visible attack volume across multiple honeypots\n-   After 2 weeks: thousands of events per day, multiple countries\n\n![](images/media/image59.png) ![](images/media/image60.png) ![](images/media/image61.png) ![](images/media/image62.png)\n\nEdited the inbound rules back (restricted to my IP only) when it was time to do the analysis cleanly.\n\n![](images/media/image63.png)\n\n---\n\n## 5\\. Log Decompression \u0026 Preparation\n\nT-Pot's internal logrotate compresses older logs to `.gz`. Splunk's Universal Forwarder can read `.gz` files but the ELK stack components don't always play well — and more importantly, I wanted to make sure everything was readable before pointing Splunk at it.\n\nFound all compressed log files:\n\n```bash\nsudo find /home/apostrophe/tpotce/data/*/log -name \"*.gz\"\n```\n\n![](images/media/image64.png)\n\nDecompressed everything while keeping the originals (the `-k` flag) and skipping the ELK directory (those are internal T-Pot indexes, not attack logs):\n\n```bash\nsudo find /home/apostrophe/tpotce/data \\\n  -path \"*/elk/*\" -prune \\\n  -o -name \"*.gz\" -exec gunzip -kf {} \\;\n```\n\n![](images/media/image65.png)\n\nVerified the output:\n\n```bash\nsudo find /home/apostrophe/tpotce/data \\\n  -path \"*/elk/*\" -prune \\\n  -o \\( -name \"*.json\" -o -name \"*.csv\" -o -name \"*.log\" \\) -print | head -10\n```\n\n![](images/media/image66.png)\n\nWorked.\n\n---\n\n## 6\\. Splunk SIEM Setup\n\n### 6.1 Installing Splunk Enterprise\n\nInstalled Splunk Enterprise on my local VM using the `.deb` package:\n\n```bash\nsudo dpkg -i splunk*.deb\ncd /opt/splunk\nsudo -u splunk ./bin/splunk start\n```\n\n![](images/media/image67.png) ![](images/media/image68.png) ![](images/media/image69.png) ![](images/media/image70.png)\n\nEnabled Splunk to start automatically on boot:\n\n```bash\nsudo /opt/splunk/bin/splunk enable boot-start -user splunk\nsudo systemctl enable Splunk\nsudo systemctl start Splunk\n```\n\nVerified it was running at `http://localhost:8000`.\n\n![](images/media/image72.png)\n\n### 6.2 Creating the Honeypot Index\n\nIn the Splunk web UI:\n\nSettings \u003e Indexes \u003e New Index\n\n| Setting | Value |\n| --- | --- |\n| Index Name | `honeypot` |\n\n![](images/media/image75.png)\n\nThen configured Splunk to receive forwarded data:\n\nSettings \u003e Forwarding and Receiving \u003e Configure Receiving \u003e New Receiving Port: **9997**\n\n![](images/media/image78.png)\n\nVerified Splunk was listening:\n\n```bash\nsudo netstat -tlnp | grep 9997\n```\n\n![](images/media/image79.png)\n\n---\n\n## 7\\. Tailscale Network Setup\n\nRather than opening a Splunk port to the internet or running a full VPN, I used **Tailscale** to create a private mesh network between the EC2 instance and my local VM. The forwarder sends to the EC2's Tailscale IP, no public exposure of Splunk at all.\n\nInstalled Tailscale on both machines:\n\n```bash\ncurl -fsSL https://tailscale.com/install.sh | sh\n```\n\n![](images/media/image80.png)\n\n```bash\nsudo tailscale up\n```\n\n![](images/media/image81.png)\n\nLogged into the same Tailscale account on both. After auth, each machine gets a stable private IP in the `100.x.x.x` range that never changes.\n\n![](images/media/image82.png) ![](images/media/image83.png) ![](images/media/image84.png)\n\nOne thing to note: I masked the Tailscale IP in the documentation screenshots since I was planning to use a Tailscale Funnel for another project, didn't want to expose that IP unnecessarily.\n\nVerified connectivity between the two machines:\n\n![](images/media/image85.png)\n\n```bash\nping \u003ctailscale-ip-of-splunk-vm\u003e\n```\n\nSuccess. The EC2 instance could now reach the Splunk receiver on the local VM over the private network.\n\n---\n\n## 8\\. Splunk Universal Forwarder\n\n### 8.1 Installation \u0026 Boot Configuration\n\nDownloaded and installed the [Splunk Universal Forwarder](https://www.splunk.com/en_us/download/universal-forwarder.html) on the EC2 instance:\n\n![](images/media/image86.png)\n\n![](images/media/image87.png)\n\n```bash\nsudo tar -xvzf splunkforwarder*.tgz\n```\n\n![](images/media/image88.png)\n\n```bash\nsudo mv splunkforwarder /opt/\ncd /opt/splunkforwarder/bin\n```\n\n![](images/media/image89.png)\n\n```bash\nsudo ./splunk start --accept-license\n```\n\n![](images/media/image90.png)\n\nI then enabled bootstart to make sure the forwarder survives reboots automatically.  \n\n```bash\nsudo ./splunk enable boot-start\n```\n\nThen I run\n\n```bash\nsudo ./splunk status  \n```\n\nbut it returned an error which I fixed by using the splunkfwd user to run those commands.\n\n```bash\nsudo -u splunkfwd ./splunk status  \n```\n\nThe forwarder was not started yet\n\n![](images/media/image91.png)\n\nPointed the forwarder at the Splunk receiver using the Tailscale IP:\n\n```bash\nsudo ./splunk add forward-server \u003ctailscale-ip-of-splunk\u003e:9997\n```\n\nbut listing the forwarders showed configured but inactive. I had to start splunkforwarder first.  \n\n![](images/media/image92.png)\n\nRunning `enable boot-start` as root, Splunk records root as the boot user in the systemd unit file and then refuses to start as the `splunkfwd` user.\n\n![](images/media/image93.png)\n\nTo fix it, this is what I did:\n\n```bash\n# Fixed ownership\nsudo chown -R splunkfwd:splunkfwd /opt/splunkforwarder\n```\n\n```bash\n# Removed the bad systemd unit\nsudo /opt/splunkforwarder/bin/splunk disable boot-start\n```\n\n```bash\n# Re-enabled with the correct user\nsudo /opt/splunkforwarder/bin/splunk enable boot-start -user splunkfwd\n```\n\n```bash\n# Started via systemd\nsudo systemctl start SplunkForwarder\nsudo systemctl status SplunkForwarder\n```\n\n![](images/media/image94.png)\n\nI then run\n\n```\n./splunk list forward-server  \n```\n\nwhich asked me to login using the username and password I had created earlier on. Now, I had an active forward.  \n\n![](images/media/image95.png)\n\n### 8.2 inputs.conf Configuration\n\nThe inputs.conf file lives at:\n\n```\n/opt/splunkforwarder/etc/apps/search/local/inputs.conf\n```\n\nNot `/opt/splunkforwarder/etc/system/local/inputs.conf` - that path exists but the apps path is where it actually takes effect. Learned that one the hard way lol.\n\n![](images/media/image96.png)\n\nThe full configuration is in [`config/inputs.conf`](config/inputs.conf) in this repo. Here's what each parameter does:\n\n| Parameter | Value | Purpose |\n| --- | --- | --- |\n| `[monitor://...]` | `/home/apostrophe/tpotce/data/*/log` | Monitors log directories for all honeypot services |\n| `disabled` | `false` | Forwarder is active |\n| `index` | `honeypot` | Routes all events to the dedicated honeypot index |\n| `followTail` | `0` | Reads existing files from byte 0 — ensures complete historical ingestion on first run, then automatically transitions to real-time tailing |\n| `crcSalt` | `\u003cSOURCE\u003e` | Includes the full file path in Splunk's checksum. Critical: ensures logrotate-numbered copies (`cowrie.json.1`, `suricata.csv.3`) are treated as unique inputs rather than duplicates of the active file |\n| `whitelist` | \u003ccode\u003e\\.(csv\\|log\\|json)(\\.\\d+)?$\u003c/code\u003e | log |\n| `sourcetype` | Per-honeypot (see config) | Each honeypot has its own sourcetype for proper field extraction |\n\n**The single-stanza mistake:** My first attempt used one wildcard stanza with `sourcetype = tpot:logs` for everything. Zero events showed up. The issue is that each honeypot produces differently structured logs - JSON, CSV, plain text and Splunk's field extraction is tied to sourcetype. Fixing it meant giving every honeypot its own stanza with its specific sourcetype like you see below\n\n![](images/media/image98.png) ![](images/media/image99.png)\n\n### 8.3 Troubleshooting Ingestion\n\nIf ingestion isn't working:\n\n```bash\n# Check forwarder is running\nsudo systemctl status SplunkForwarder\n\n# Verify forward server is configured and active\nsudo -u splunkfwd /opt/splunkforwarder/bin/splunk list forward-server\n\n# Check forwarder logs for errors\nsudo tail -f /opt/splunkforwarder/var/log/splunk/splunkd.log\n\n# Test connectivity to Splunk receiver\nnc -zv \u003ctailscale-ip\u003e 9997\n```\n\nIn Splunk, confirm events are arriving:\n\n```spl\nindex=honeypot | head 10\n```\n\n---\n\n## 9\\. Claude AI + Splunk MCP Integration\n\nThis is the part that makes this project a bit different from the usual T-Pot write-up. Instead of just clicking around Kibana, I connected Claude AI directly to Splunk using the **Model Context Protocol (MCP)** which lets Claude issue real Splunk queries, read the results, and reason over them in natural language. Most importantly, I did this as a test for my SOC automation project which I plan to rather use a local AI deployment as my SOC assistant.\n\n### 9.1 Installing Claude Desktop (Linux)\n\nClaude Desktop doesn't have an official Linux build yet, so used the community Debian release:\n\n```bash\n# Prerequisites\nsudo apt update \u0026\u0026 sudo apt upgrade -y\n```\n\n![](images/media/image100.png)\n\n```bash\n# Node.js (required for MCP servers)\ncurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -\nsudo apt install -y nodejs\n```\n\n![](images/media/image101.png)\n\n```bash\n# Verify\nnode --version\nnpx --version\n```\n\n![](images/media/image102.png)\n\nDownloaded the `.deb` from: [https://github.com/aaddrick/claude-desktop-debian/releases](https://github.com/aaddrick/claude-desktop-debian/releases)\n\n![](images/media/image104.png)\n\n```bash\nsudo dpkg -i claude-desktop-*-amd64.deb\n```\n\n![](images/media/image105.png) ![](images/media/image106.png) ![](images/media/image109.png)\n\n### 9.2 Splunk MCP Server Setup\n\nIn Splunk Enterprise:\n\n**1. Install the Splunk MCP Server app** from Splunkbase (search: \"MCP Server\")\n\n![](images/media/image110.png)\n\n**2. Create an `mcp_user` role:**\n\nSettings \u003e Roles \u003e New Role\n\n-   Inherit from: `user`\n-   Add capabilities: all `mcp_*` capabilities\n\n![](images/media/image112.png) ![](images/media/image113.png) ![](images/media/image114.png)\n\n**3. Create an `mcp_user_1` account and assign it the `mcp_user` role**\n\n![](images/media/image115.png) ![](images/media/image116.png) ![](images/media/image117.png)\n\n**4. Generate an MCP authentication token** for `mcp_user_1`:\n\nSettings \u003e Tokens \u003e New Token\n\n![](images/media/image118.png) ![](images/media/image119.png) ![](images/media/image121.png)\n\nCopy the token immediately. Once you close that page it's gone forever and you'll have to generate a new one.\n\n**5. Restart Splunk** (Settings \u003e Server Controls \u003e Restart Splunk)\n\n### 9.3 MCP Configuration File\n\nThe Claude Desktop config lives at:\n\n```\n~/.config/Claude/claude_desktop_config.json\n```\n\nThe full config.. with Virustotal MCP is in [`config/claude_desktop_config.json`](config/claude_desktop_config.json). Below shows that for Splunk.\n\n```json\n{\n  \"mcpServers\": {\n    \"splunk-mcp-server\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"mcp-remote\",\n        \"https://192.168.56.112:8089/services/mcp\",\n        \"--header\",\n        \"Authorization: Bearer \u003ctoken\u003e\"\n      ],\n      \"env\": {\n        \"NODE_TLS_REJECT_UNAUTHORIZED\": \"0\"\n      }\n    }\n  }\n}\n```\n\n![](images/media/image122.png)\n\nNote that the token must be pasted after Bearer as seen below. Save it afterwards\n\n![](images/media/image123.png)\n\nAfter saving the config, restart Claude Desktop:\n\n```bash\nsudo pkill -f claude-desktop\nclaude-desktop \u0026\n```\n\n![](images/media/image125.png) ![](images/media/image126.png)\n\nWhat data do I have currently indexed in splunk? ![](images/media/image127.png)  \nQuery the honeypot index and identify the top 10 IPs performing port scanning activity. For each IP, show which ports they scanned and which honeypots they triggered, ordered by total event count.  \n\n![](images/media/image128.png)\n\n---\n\n## 10 Splunk Queries Detection Engineering Reference\n\nAll queries run against `index=honeypot`. These are the core queries used throughout the analysis and serve as a starting point for detection rule development.\n\n### 10.1 Discovery Queries\n\n![](images/media/image129.png)\n\n```spl\n-- Total unique IPs and events (application-layer honeypots)\nindex=honeypot sourcetype!=p0f:log sourcetype!=suricata:json sourcetype!=fatt:json\n| stats dc(src_ip) as unique_ips, count as total_events\n\n-- Total unique IPs and events (Suricata IDS, external only)\nindex=honeypot sourcetype=suricata:json NOT src_ip=\"172.31.*\" NOT src_ip=\"169.254.*\"\n| stats dc(src_ip) as unique_ips, count as total_events\n```\n\n### 10.2 Attack Surface Queries\n\n![](images/media/image130.png)\n\n```spl\n-- Top 20 attacking IPs\nindex=honeypot sourcetype!=p0f:log sourcetype!=suricata:json sourcetype!=fatt:json\n| stats count by src_ip | sort -count | head 20\n\n-- Geographic distribution (top 15 countries)\nindex=honeypot sourcetype!=p0f:log sourcetype!=suricata:json\n| iplocation src_ip | stats count by Country | sort -count | head 15\n\n-- Target port distribution (Suricata, external IPs)\nindex=honeypot sourcetype=suricata:json NOT src_ip=\"172.31.*\" NOT src_ip=\"169.254.*\"\n| stats count by dest_port | sort -count | head 15\n\n-- Top Suricata signatures fired\nindex=honeypot sourcetype=suricata:json NOT src_ip=\"172.31.*\" NOT src_ip=\"169.254.*\"\n| stats count by alert.signature_id, alert.signature | sort -count | head 20\n\n-- CVE extraction from Suricata alerts\nindex=honeypot sourcetype=suricata:json alert.signature=*CVE* NOT src_ip=\"172.31.*\"\n| rex field=alert.signature \"(?\u003ccve\u003eCVE-\\d{4}-\\d+)\"\n| stats count by cve | sort -count\n\n-- Blocklist hit rate (Dshield / Spamhaus / CINS)\nindex=honeypot sourcetype=suricata:json\n  (alert.signature=\"*Dshield*\" OR alert.signature=\"*Spamhaus*\" OR alert.signature=\"*CINS*\")\n  NOT src_ip=\"172.31.*\"\n| stats dc(src_ip) as blocklisted_ips, count as events\n\n-- p0f OS fingerprinting (attacker OS distribution)\nindex=honeypot sourcetype=p0f:log mod=\"syn\" subject=\"cli\" os!=\"???\" os!=\"\"\n| stats count by os | sort -count | head 15\n```\n\n### 10.3 Cowrie SSH/Telnet Queries\n\n![](images/media/image131.png)\n\n```spl\n-- All successful logins\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.login.success\"\n| table _time, src_ip, username, password | sort _time\n\n-- Top credential pairs attempted (brute-force wordlist analysis)\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.login.failed\"\n| stats count by username, password | sort -count | head 20\n\n-- Post-exploitation commands\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.command.input\"\n| stats count by input | sort -count | head 30\n\n-- Malware payload hashes (file downloads via Cowrie)\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.session.file_download\"\n| table _time, src_ip, url, shasum, outfile | sort _time\n\n-- Full timeline for a specific IP\nindex=honeypot sourcetype=cowrie src_ip=\"\u003cTARGET_IP\u003e\"\n| sort _time | table _time, eventid, username, password, input, url, shasum\n```\n\n### 10.4 Threat Intelligence Queries\n\n![](images/media/image133.png)\n\n```spl\n-- DoublePulsar/EternalBlue scanning — top source IPs\nindex=honeypot sourcetype=suricata:json alert.signature=\"*DoublePulsar*\" NOT src_ip=\"172.31.*\"\n| stats count by src_ip | sort -count | head 15\n\n-- ICS/SCADA protocol targeting (IEC-104)\nindex=honeypot sourcetype=suricata:json\n  (alert.signature=\"*SCADA*\" OR alert.signature=\"*IEC-104*\")\n  NOT src_ip=\"172.31.*\"\n| stats count by alert.signature, src_ip | sort -count\n\n-- Cobalt Strike JARM candidates (multi-source check)\nindex=honeypot\n  (src_ip=\"18.218.118.203\" OR src_ip=\"3.132.26.232\" OR\n   src_ip=\"3.130.168.2\" OR src_ip=\"3.129.187.38\")\n| stats count by src_ip, sourcetype | sort -count\n\n-- Redis honeypot top attackers\nindex=honeypot sourcetype=redishoneypot:log\n| rex field=_raw \"(?\u003csrc_ip\u003e\\d+\\.\\d+\\.\\d+\\.\\d+)\"\n| stats count by src_ip | sort -count | head 10\n\n-- Tanner web honeypot — top URL paths targeted\nindex=honeypot sourcetype=tanner:json | stats count by path | sort -count | head 20\n\n-- Tanner web honeypot — user-agent analysis\nindex=honeypot sourcetype=tanner:json\n| stats count by method, \"headers.user-agent\" | sort -count | head 15\n\n-- ADB honeypot source IPs\nindex=honeypot sourcetype=adbhoney\n| rex field=_raw \"(?\u003csrc_ip\u003e\\d+\\.\\d+\\.\\d+\\.\\d+)\"\n| stats count by src_ip | sort -count | head 10\n```\n\n### 10.5 Campaign \u0026 Attribution Queries\n\n![](images/media/image134.png)\n\n```spl\n-- IONOS fleet detection (coordinated VPS campaign)\nindex=honeypot\n  (src_ip=\"31.70.75.115\" OR src_ip=\"31.70.89.209\" OR src_ip=\"31.70.75.109\"\n   OR src_ip=\"31.70.75.117\" OR src_ip=\"31.70.78.114\" OR src_ip=\"31.70.78.222\"\n   OR src_ip=\"31.70.75.104\" OR src_ip=\"31.70.75.118\" OR src_ip=\"31.70.77.205\")\n| stats count by src_ip | sort -count\n\n-- 2026-05-24 mass compromise wave timeline\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.login.success\"\n| eval date=strftime(_time,\"%Y-%m-%d\") | where date=\"2026-05-24\"\n| sort _time | table _time, src_ip, username, password\n\n-- mdrfckr SSH key injection detection\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.command.input\"\n  input=\"*mdrfckr*\" OR input=\"*chattr -ia .ssh*\"\n| stats count by src_ip | sort -count\n\n-- Competing malware cleanup command (anti-competition behaviour)\nindex=honeypot sourcetype=cowrie eventid=\"cowrie.command.input\"\n  input=\"*pkill -9 secure.sh*\" OR input=\"*hosts.deny*\"\n| stats count by src_ip | sort -count\n```\n\n---\n\n## 11. Kibana Dashboard Queries\n\nT-Pot's built-in Kibana dashboards are excellent for visual exploration. The most useful queries for digging beyond the defaults:\n\n```\n-- ECS-style field for source IP (T-Pot normalises to this)\nsrc_ip: *\n\n-- Filter to specific honeypot\ntype: \"Cowrie\"\ntype: \"Dionaea\"\ntype: \"ADBHoney\"\ntype: \"Suricata\"\n\n-- Successful Cowrie logins only\ntype: \"Cowrie\" AND eventid: \"cowrie.login.success\"\n\n-- Post-exploitation commands\ntype: \"Cowrie\" AND eventid: \"cowrie.command.input\"\n\n-- EternalBlue/DoublePulsar alerts only\ntype: \"Suricata\" AND alert.signature: *DoublePulsar*\n\n-- CVE-specific filter\ntype: \"Suricata\" AND alert.signature: *CVE-2024-4577*\n\n-- Specific source IP investigation\nsrc_ip: \"81.9.145.130\"\n\n-- File downloads (malware delivery)\ntype: \"Cowrie\" AND eventid: \"cowrie.session.file_download\"\n```\n\n---\n\n## 12. OPNsense Firewall Installation\n\n**NOTE: From section 12 to 14.. on the OPNsense configuration, I wrote the full method on my **[medium](https://medium.com/cybersecuritywriteups/how-to-secure-your-homelab-setup-with-opnsense-firewall-in-virtualbox-installation-threat-a54a12fff536)** page. It contains the step by step procedure with images attached. Hence, I strongly recommend you check that how if you do not understnd a particular part (from Scetion 12 to 15)**\n\n### 12.1 VM Setup and OS Installation\n \nDownloaded the OPNsense ISO from [opnsense.org/download](https://opnsense.org/download/) — architecture `amd64`, image type `dvd`, mirror set to OPNsense and extracted the compressed ISO when it was done downloading before use.\n \nCreated a new VirtualBox VM:\n \n| Setting | Value |\n|---|---|\n| OS Type | FreeBSD (64-bit) |\n| RAM | 2 GB |\n| CPU | 2 vCPUs |\n| Storage | 10 GB |\n| Network adapters | Adapter 1: NAT (WAN) · Adapter 2: Host-only (LAN) |\n \nBooted from the ISO and logged in as `installer` (password: `installer`) to start the installation. Selected the default keymap, then assigned interfaces;  WAN to the NAT adapter, LAN to the host-only adapter.\n \n### 12.2 Interface Configuration and Web Access\n \nAfter installation, the web interface was initially unreachable. The fix was to restart OPNsense fully before attempting to connect; the interfaces needed a clean initialisation after first boot lol.\n \nChanged the LAN IPv4 address to `192.168.56.144` from the console menu. For this setup, I switched the web interface from HTTPS to HTTP (option available in the console menu) to avoid certificate prompts. In a production environment HTTPS stays on. I then accessed the OPNsense web interface.\n \n---\n \n## 13. Threat Intelligence Feed \u003e OPNsense Alias\n \n### 13.1 Serving the Blocklist over HTTP\n \nRather than manually pasting IPs into OPNsense, the `ip-blocklist.txt` file from this project is served over HTTP from a simple Python server on the host machine. OPNsense pulls from it as a URL alias — the list stays live without touching the firewall config.\n \nOn the host machine, I navigate to the directory containing `ip-blocklist.txt` and run:\n \n```bash\npython3 -m http.server 6050\n```\n \n### 13.2 Creating the Tpot_threatintel Alias\n \nIn OPNsense: **Firewall \u003e Aliases \u003e click the + icon (bottom right)**\n \n| Field | Value |\n|---|---|\n| Name | `Tpot_threatintel` |\n| Type | `URL Table (IPs)` |\n| Content | `http://192.168.56.143:6050/ip-blocklist.txt` |\n| Description |  |\n| Refresh Frequency | 1 day  |\n \nClicked **Save** then **Apply Changes**.\n \nVerified the alias loaded correctly: **Firewall \u003e Diagnostics \u003e Aliases \u003e select `Tpot_threatintel`**\n \nThe list of IP addresses from the blocklist should appear. If the list is empty, check that the Python HTTP server is running and reachable from OPNsense. (Again, you can check my writeup on medium if you do not understand)\n \n### 13.3 Firewall Rules WAN and LAN\n \nTwo rules, one blocking inbound attacks from blocklisted IPs, one blocking outbound connections to them (C2 prevention).\n \n**WAN Rule - block inbound from blocklist:**\n \n**Firewall \u003e Rules \u003e WAN \u003e Add (up arrow — top of list)**\n \n| Field | Value |\n|---|---|\n| Action | Block |\n| Interface | WAN |\n| Direction | in |\n| Protocol | any |\n| Source | `Tpot_threatintel` (alias) |\n| Destination | any |\n| Description | Block T-Pot IOC IPs inbound |\n \n**LAN Rule - block outbound to blocklist:**\n \n**Firewall \u003e Rules \u003e LAN \u003e Add (up arrow)**\n \n| Field | Value |\n|---|---|\n| Action | Block |\n| Interface | LAN |\n| Direction | **in** \u003c\u003c\u003c important: not `out` |\n| Protocol | any |\n| Source | LAN net |\n| Destination | `Tpot_threatintel` (alias) |\n| Description | Block outbound to T-Pot IOC IPs |\n \n\u003e The LAN rule must be set to `in` — meaning traffic *entering* the LAN interface from LAN clients toward the internet. Setting it to `out` means traffic *leaving* the LAN interface toward the internet, which is after routing has already happened. The rule would never fire. This was confirmed through troubleshooting hence changing from `out` to `in` is what made the block work.\n \nApply changes after creating both rules and test by attempting to ping a blocklisted IP from a LAN device, it should be blocked and the firewall log should show the match.\n \n### 13.4 DNS Sinkhole for Malicious Domains\n \nThe C2 domains identified during the honeypot observation period are blocked at DNS resolution level using OPNsense's built-in Unbound DNS blocklist feature. Any device on the network querying a sinkholed domain gets a dead response, the connection never leaves.\n \n**Services \u003e Unbound DNS \u003e Blocklist**\n \nEnabled the blocklist and added the domains from [`iocs/c2-domains-urls.txt`](iocs/c2-domains-urls.txt) in this repo. The three confirmed malicious domains from the observation period are listed there.\n \n\u003e **WannaCry killswitch exception:** I did **not** add `iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com` to the DNS sinkhole. Blocking this domain will reactivate WannaCry encryption on any infected host that tries to resolve it. Monitor DNS queries for this domain instead — a query means there is an infected host on the network.\n \n---\n \n## 14. Suricata IDS/IPS Custom Rules Deployment\n \n### 14.1 Enabling Suricata in IPS Mode\n \n**Services \u003e Intrusion Detection \u003e Administration**\n \n| Setting | Value |\n|---|---|\n| Enabled | ✅ |\n| IPS Mode | ✅  |\n| Interface | WAN |\n| Pattern Matcher | Hyperscan |\n \nHyperscan is Intel's high-performance regex engine — faster than the default Aho-Corasick on modern hardware.\n\n \n### 14.2 Deploying Custom Rules via SFTP\n \nThe custom Suricata rules built from honeypot IOCs (in [`suricata-rules/tpot-custom.rules`](suricata-rules/tpot-custom.rules)) need to be transferred to the OPNsense FreeBSD filesystem directly. OPNsense does not expose a built-in way to upload arbitrary rule files through the web UI.\n \n**Step 1: Enable SSH on OPNsense:**\n \n**System \u003e Settings \u003e Administration \u003e Secure Shell \u003e Enable**\n \n**Step 2: Prepare the XML metadata file**\n \nOPNsense's IDS engine discovers downloadable rulesets through XML metadata files in `/usr/local/opnsense/scripts/suricata/metadata/rules/`. Create a `tpot_ruleset.xml` file and serve it alongside the rules from a Python HTTP server:\n \n```xml\n\u003c?xml version=\"1.0\"?\u003e\n\u003cruleset documentation_url=\"http://docs.opnsense.org/\"\u003e\n    \u003clocation url=\"http://192.168.56.1:80/\" prefix=\"tpot\"/\u003e\n    \u003cfiles\u003e\n        \u003cfile description=\"T-Pot IP Drop Rules - Confirmed attacker IPs\" url=\"inline::rules/tpot-ip-drop.rules\"\u003etpot-ip-drop.rules\u003c/file\u003e\n        \u003cfile description=\"T-Pot C2 Alert Drop Rules - C2 URLs, JARM, DNS\" url=\"inline::rules/tpot-c2-alert-drop.rules\"\u003etpot-c2-alert-drop.rules\u003c/file\u003e\n    \u003c/files\u003e\n\u003c/ruleset\u003e\n```\n \n**Step 3 — Transfer files via SFTP**\n \nThe SSH copy command had permission errors when attempted directly hence I used FileZilla (or any SFTP client) instead:\n \n- Host: `192.168.56.144`\n- Username: `root`\n- Password: enterred my password here\n- Port: `22`\nTransfered `tpot_ruleset.xml` to:\n```\n/usr/local/opnsense/scripts/suricata/metadata/rules/\n```\n \nAnd transfered `tpot-custom.rules` to:\n```\n/usr/local/etc/suricata/rules/\n```\n \n**Step 4: Apply in OPNsense**\n \n**Services \u003e Intrusion Detection \u003e Administration \u003e restart the service**\n \n**Services \u003e Intrusion Detection \u003e Download** - the custom ruleset should now appear. Download it, then verify the rules loaded:\n \n**Services \u003e Intrusion Detection \u003e Rules** - search for rules with the `tpot` prefix to confirm they are listed.\n \n### 14.3 Routing Traffic Through OPNsense\n \nFor LAN devices to have their traffic inspected and filtered by OPNsense, all traffic must route through the firewall, not bypass it via a secondary NAT interface.\n \nA common issue in VirtualBox setups: a VM has both a Host-Only adapter (going through OPNsense) and a NAT adapter (going directly to the internet). Traffic prefers the NAT adapter and never touches OPNsense.\n \n**Fix: remove the NAT adapter and route everything through OPNsense**\n \n```bash\n# Check current routing table\nip route show\n \n# Remove the direct NAT route (enp0s3 / 10.0.2.x)\nsudo ip route del default\n \n# Route everything through OPNsense LAN gateway\nsudo ip route add default via 192.168.56.143 dev enp0s8\n \n# Verify\nip route show\n```\n \n**Fix DNS: set OPNsense as the DNS server**\n \n```bash\nsudo nano /etc/resolv.conf\n```\n \n```\nnameserver 192.168.56.143\n```\n \n**Fix NAT: configure outbound masquerading in OPNsense**\n \n**Firewall \u003e NAT \u003e Outbound \u003e switch to Manual outbound NAT rule generation \u003e Save**\n \nAdd a rule: Interface WAN, Source LAN net, Translation: WAN address. This allows LAN devices to reach the internet through OPNsense.\n \nAfter these changes, verify with a ping to a known-good IP (should succeed) and a ping to a blocklisted IP (should be blocked and visible in the firewall log).\n \n---\n\n## 15. DFIR-IRIS Ticketing System Integration\n**NOTE:** I have a medium writeup on this section as well. It contains the full steps with images. aRead it **[here](https://medium.com/@princelassey/how-to-deploy-dfir-iris-incident-response-ticketing-system-in-virtualbox-into-your-homelab-setup-7661b58e2b9a)**\n \n### 15.1 Installation via Docker Compose\n \nOn the N8N-DFIR VM:\n \n```bash\ngit clone https://github.com/dfir-iris/iris-web.git\ncd iris-web\ngit checkout v2.4.27\ncp .env.model .env\ndocker compose pull\n```\n \n### 15.2 Environment Configuration\n \n```bash\nsudo nano .env\n```\n \nGenerate all secrets before editing. Never reuse values between fields:\n \n```bash\n# Database passwords\nopenssl rand -hex 24\n \n# IRIS secret key (longer — used for session signing)\nopenssl rand -hex 32\n \n# Password salt\nopenssl rand -hex 16\n```\n \nKey fields to set in `.env`:\n \n| Field | Generator | Notes |\n|---|---|---|\n| `POSTGRES_PASSWORD` | `openssl rand -hex 24` | Main DB password |\n| `IRIS_SECRET_KEY` | `openssl rand -hex 32` | Session signing key |\n| `IRIS_SECURITY_PASSWORD_SALT` | `openssl rand -hex 16` | Password hashing salt |\n| `IRIS_ADM_PASSWORD` | Custom | Uncomment and set a strong admin password |\n| `IRIS_ADM_API_KEY` | `openssl rand -hex 32` | Uncomment - this is what N8N uses to authenticate |\n| `IRIS_ADM_EMAIL` | Custom | Admin email address |\n \nStart the stack:\n \n```bash\ndocker compose up -d\n```\n \nAccess IRIS at `https://localhost`, accept the self-signed certificate. Log in with the admin credentials set in `.env`.\n \n**Post-install: add your analyst account**\n \n1. Go to **Advanced** (top right menu) \u003e **Users**\n2. Add your user account\n3. Navigate to **Advanced \u003e User groups** \u003e add the new account to the **Analysts** group\nCopy the API key from your user profile - this is what goes into N8N's IRIS HTTP Request nodes as the `Bearer` token.\n\n---\n\n## 16. N8N SOAR - Installation and Workflow Setup\n\n**NOTE:** I have a medium writeup on this section as well. It contains the full steps with images. Read it **[here](https://medium.com/@princelassey/integrating-an-ai-powered-security-orchestration-workflow-into-your-homelab-setup-with-n8n-splunk-ea3cb616d8af)**\n\n### 16.1 Installing N8N via Docker Compose\n \nOn the N8N-DFIR VM, I created the working directory and compose file:\n \n```bash\nmkdir n8n\ncd n8n\nnano compose.yaml\n```\n \n`compose.yaml`: [`n8n-compose.yaml`](config/n8n-compose.yaml)\n \nBrowse to `http://localhost:5678`, set your account password, and activate the free premium features with the emailed key under **Settings \u003e Usage and Plan**.\n\n \n### 16.2 Connecting Ollama from Windows Host\n \nOllama runs on my Windows host machine. N8N on the VirtualBox VM needs to reach it on port `11434`. By default, Windows Firewall blocks this. Hence, I opened PowerShell **as Administrator** on the Windows host:\n \n```powershell\nNew-NetFirewallRule `\n  -DisplayName \"Ollama 11434\" `\n  -Direction Inbound `\n  -Protocol TCP `\n  -LocalPort 11434 `\n  -Action Allow\n```\n \nThen verified from the N8N VM:\n \n```bash\ncurl -v http://192.168.56.1:11434/api/tags\n```\n \nIt should return the list of available models. If it fails, check that Ollama is running (`ollama serve`) and that the firewall rule was created for the correct profile (Domain/Private/Public).\n \n\u003e **Note:** In this project, the AI agent ended up using the **Gemma4:31b** model due to its superior instruction-following for structured report generation. Other smaller models strucgles to call the Virustotal and IPAPI tolols and even could not generate the alert summary. I realised that the prompt may have to be compressed into one user prompt before a model like qwen3.5 can be ablee to generate something meaningful.\n \n### 16.3 Building the Splunk Alert Trigger\n \nTwo Splunk saved searches were created as the pipeline entry points:\n \n**Alert 1 - Elasticsearch Ransom Note Attempt:**\n \n**Alert 2 - Successful Login after Bruteforce:**\n \n\n \n**Alert configuration for both:**\n \n- **Trigger condition:** Number of results is greater than 0\n- **Schedule:** Cron — `* * * * *` (every minute)\n- **Trigger actions:**\n  - Add to Triggered Alerts\n  - Webhook \u003e URL: `http://\u003cn8n-tailscale-ip\u003e:5678/webhook/2602f6d7-039...`\nAfter saving the alert, trigger it manually once and **pin the test event in N8N** - this gives you all the field names to reference when building the rest of the workflow without waiting for a live alert.\n \n### 16.4 AI Triage Workflow - Ollama + VirusTotal + IPAPI\n \nThe full N8N workflow JSON is available in [`n8n-workflows/triage-pipeline.json`](n8n-workflows/triage-pipeline.json). Import it directly into N8N via **Workflows \u003e Import from file**.\n \n \n**AI Agent system prompt** (abbreviated - full prompt in the workflow JSON as well):\n \n**Role: User**\n```\nAlert Title: {{ $json.body.search_name }} \nSource IP: {{ $json.body.result.src_ip }}\nAffected Host: {{ $json.body.result.host }}\nAlert Payload: {{ JSON.stringify($json.body.result, null, 2) }}\nEnrich all IOCs using virustotal and IPAPI then produce the full report\n```\n\n**Role: Assistant**\n```\nWhen alert data is provided, you will extract every IOC present, enrich each one using the Virustotal and IPAPI tools, and then produce the investigation report below. I begin directly with the report and add nothing before or after it.\n\n*ALERT SUMMARY*\n*---------------------*\nBegin by clearly summarizing what triggered the alert and the nature of the activity \nobserved. Describe the affected user, host, system, or asset involved, along with the \nrelevant source details such as IP address, hostname, username, or process if present in \nthe alert and the time it happened. Explain the type of activity detected, such as authentication failures, \nsuspicious execution, malware detection, privilege escalation, lateral movement, or any \nother notable behavior reflected in the provided data. Keep this section brief.\n\n*ALERT DETAILS*\n*-----------------*\n_Alert Type_        :\n_Alert Source_    :\n_Severity_           :\n_Source IP_         :\n_Destination IP_  :\n_Affected Host_  :\n_Affected User_  :\n_Process/File_    :\n_First Seen_        :\n_Last Seen_        :\n\nOmit any row where the value is not available in the alert data and add rows in the alert data which is not present above.\n\n*IOC ENRICHMENT*\n*---------------------*\nExtract all relevant indicators from the alert, including IP addresses, domains, URLs, \nfile hashes, email addresses, filenames, or other observable artifacts. For each indicator \nfound, enrich it using the available tools and describe the results in plain text. Write \neach IOC as a labeled block in this format:\n\n[IOC TYPE: value]\n_VirusTotal_: Write into details the detection count, overall verdict, any notable vendor flags and every important info you get from Virustotal such as what the community associates the IP to (if malware, what specific malware?). Write into  details the info you get from virustotal's detection, details and community sub-sections.\n\n_IPAPI_: Provide every information possible about the geolocation of the IP such as the country, city, ISP, ASN, and whether the IP belongs to a hosting provider, VPN, or known cloud range. Include IPAPI results for IP addresses only.\n\nIf the alert content suggests that an IOC is malicious, suspicious, or linked to malware \nor a known threat actor, include that assessment clearly and explain its relevance to the \ninvestigation. If no enrichable indicators are found, state that explicitly and continue.\n\n*INVESTIGATION FINDINGS*\n*----------------------------*\nReview the provided logs, event data, and contextual information to identify the most \nimportant findings. Describe the key observations from the alert, including repeated \nbehavior, unusual patterns, event frequency, or anomalies that stand out. If a sequence \nof events can be derived from the data, explain the timeline in a logical order and connect \nthe events in a way that helps clarify what happened. Highlight any evidence suggesting \ncompromise, malicious intent, or suspicious activity, and explain how the observed data \nsupports that assessment. Keep this section brief.\n\n*MITRE ATT\u0026CK MAPPING*\n*---------------------------*\nMap the observed behavior to the MITRE ATT\u0026CK FRAMEWORK with the tactics and techniques \nsupported by the alert data. For each mapping, include the tactic name, technique name, \nand technique ID, and briefly explain why each mapping applies based on the activity seen \nin the alert. Focus on mappings that best align with the actual behavior observed rather \nthan listing techniques too broadly. Write each entry on its own line in this format:\n\n_Tactic_: [tactic name]\n_Technique_: [technique name]\n_ID_: [ATT\u0026CK ID]\n_Reason_: [one sentence explaining why this applies]\n\n*SEVERITY ASSESSMENT*\n*--------------------------*\nAssign a severity rating of Low, Medium, High, or Critical based on the nature of the \nalert and the evidence available. In your assessment, consider the potential impact of the \nactivity, the likelihood that the behavior represents real malicious activity, and the \nimportance or sensitivity of the affected asset, account, or environment. Provide a short \nexplanation that justifies the chosen severity level using the alert details.\n\n_Rating_        :\n_Justification_ :\n\n*RECOMMENDED ACTIONS*\n*----------------------------*\nProvide practical and actionable response recommendations that a SOC analyst could follow. \nDescribe immediate containment actions where appropriate, such as blocking an IP, isolating \na host, disabling an account, or restricting access. Also include mitigation steps that \nwould reduce the risk of recurrence, such as password resets, MFA enforcement, malware \nremoval, or policy changes, along with monitoring or follow-up actions that would help \nvalidate the extent of the activity or detect related behavior. Write the recommendations using bullets below\n\n-\n-\n-\n\n*CONCLUSION*\n*---------------*\nConclude the report by suummarizing what the alert was about then classify the alert as a True Positive, False Positive, Benign \nTrue Positive, or Undetermined. Support that classification with a brief justification \nthat reflects the overall evidence, observed behavior, and level of confidence derived \nfrom the alert analysis.\n\n{Conclusion}\n- _Verdict_:\n- _Justification_:\n\n```\n\n**Role: System Message**\n```\nYou are a Tier 1 SOC Analyst Assistant working in a Security Operations Center (SOC). You will be provided with a security alert which may include logs, indicators of compromise, metadata, or SIEM \noutput. Analyze the provided information and generate a structured SOC investigation \nreport rummary based on the evidence contained in the alert.\n\nYou have access to the following tools for threat intelligence enrichment:\n- Virustotal: Use this to gain extensive information for IP addresses, domains, and file hashes\n  IP:     GET https://www.virustotal.com/api/v3/ip_addresses/{ip}\n  Domain: GET https://www.virustotal.com/api/v3/domains/{domain}\n  Hash:   GET https://www.virustotal.com/api/v3/files/{hash}\n- IPAPI: Use this to gaiin extensive information for geolocation and ASN enrichment on any source IP address\n\n\nWhen extracting and classifying IOCs from the alert, follow this priority order:\n- An IPv4 address in X.X.X.X format should be queried against the VirusTotal IP endpoint \nand enriched with IPAPI. \n- A 64-character hex string is a SHA256 hash, 40 characters is \nSHA1, and 32 characters is MD5 - all three use the VirusTotal files endpoint. \n- An email is in the form example@example.com\n- If you encounter a URL beginning with http or https, extract the domain and query the VirusTotal \ndomain endpoint rather than submitting the full URL. \n- A bare hostname with no scheme is treated as a domain and queried directly. Never classify an IP address as a domain or URL, and never submit a full URL to VirusTotal. \n- Enrich every IOC you find before writing the report. \n- If enrichment is unavailable for any indicator, record it as Unknown. \n- Only perform GET requests to VirusTotal.\n- DO NOT PERFORM ENRICHMENT ON TARGET IP/ DESTINATION IP since that IP will mostly be an internal system.\n\nYour output must be plain text only. Do not use Markdown, hashtags, asterisks, bullet \nsymbols, or backticks anywhere in the response. Use ALL CAPS for section headings and \nplain dashes as dividers. Do not add any preamble before the report or any closing remarks \nafter it. Begin your response immediately with the ALERT SUMMARY heading.\n\n```\n\n \n### 16.5 Auto-Block on True Positive Verdict\n \nAfter the AI produces its report, an **IF node** checks the output for the verdict:\n \n```\nCondition: {{ $json.output.includes('True Positive') }}\n```\n \n**TRUE branch \u003e OPNsense block sequence:**\n \n```\nIF (True Positive)\n    └─► HTTP Request - OPNsense: add IP to alias\n    │     POST https://192.168.56.144/api/firewall/alias/addItem/Dynamic_Blocklist\n    │     Auth: Basic (api_key:api_secret base64)\n    │     Body: { \"address\": \"{{ source_ip }}\" }\n    │\n    └─► HTTP Request - OPNsense: apply rule\n          POST https://192.168.56.144/api/firewall/alias/reconfigure\n```\n \n**FALSE branch \u003e ends the workflow** - the alert was sent to IRIS and Slack, but no block is applied.\n \nConfirmed working: after the Elasticsearch ransom note alert fired with a True Positive verdict, IP `105.27.148.94` was automatically added to the `Dynamic__Blocklist` alias and the firewall rules applied, verified in **Firewall \u003e Diagnostics \u003e Aliases \u003e Tpot_threatintel**.\n \n### 16.6 DFIR-IRIS Case Creation and Slack Notification\n \nThe enriched AI report is sent to DFIR-IRIS as an **alert** (not a case directly). Alerts in IRIS represent incoming events that analysts review and can escalate into full cases. After receiving the enriched alert in IRIS, the investigation workflow is:\n \n1. Review the AI report and VirusTotal enrichment in the alert\n2. Escalate to a **Case** if investigation is needed beyond the auto-block\n3. Extract IOCs using the IRIS IOC module\n4. Add investigation notes as the case progresses\n5. Close the case with final verdict and summary\n\n---\n\n\n\n## 18. Threat Intelligence Report\n\nThe full threat intelligence report for this deployment is available here:\n\n**[T-Pot Threat Intelligence Report v2.0 (PDF)](report/T-Pot_Threat_Intelligence_Report_v2.pdf)**\n\nThe report covers  includes:\n\n-   Attack volume and geographic breakdown\n-   Infrastructure classification of 100 attacking IPs via VirusTotal\n-   Full CVE analysis across 12 Suricata-detected vulnerabilities\n-   VirusTotal analysis of all 51 malware hashes (Cowrie, ADBHoney, Dionaea)\n-   Reconstructed timelines for 5 confirmed attacker sessions\n-   Attribution of coordinated campaigns (IONOS VPS fleet, mdrfckr SSH worm, Cobalt Strike JARM cluster)\n-   Human-vs-automated attacker behaviour analysis\n-   ICS/SCADA targeting findings (IEC-104 protocol against ConPot)\n-   Full IOC table (IPs, hashes, SSH backdoor key, credentials, C2 domains)\n-   MITRE ATT\u0026CK technique mapping\n-   Detection engineering recommendations\n\n---\n\n## 19. Key Findings Summary\n\nA few things that stood out from the data:\n\n**The mdrfckr cryptomining worm is everywhere.** 14 separate IPs dropped the same mdrfckr SSH backdoor key across the observation period. This is a worm that's been active since at least 2019 and is still running. Its post-exploitation playbook (strip immutable flags, inject key, kill competing malware, check CPU resources) is fully scripted and highly consistent. If you have internet-facing SSH anywhere, check your `authorized_keys` files.\n\n**WannaCry is still alive.** All 22 malware samples captured by Dionaea via EternalBlue were WannaCry variants - 88–95% detection rates on VirusTotal, most still contacting the original killswitch domain. In 2026. Nine years after the 2017 outbreak. Unpatched Windows SMBv1 nodes are still genuinely common on the internet.\n\n**Someone knows ICS protocols.** IP `18.218.118.203` carries a Cobalt Strike JARM fingerprint AND probed ConPot with IEC 60870-5-104 (power grid SCADA protocol) across honeypot days. That combination of a red-team framework fingerprint on an IP that understands industrial control system protocols is worth flagging to ICS-CERT if you're operating in that space.\n\n**Three malware samples don't exist on VirusTotal.** Hashes `7aa7aae3...`, `f7a2eec2...`, and `93d7393...` from Cowrie are genuinely absent. Original, uncatalogued samples sitting on the honeypot - which is exactly what honeypots are for.\n\n**A human might have been in the box.** IP `40.112.183.29` (Azure) ran `w` and `top` manually across three separate sessions with 6–11 minute natural gaps between them. Every other IP in the dataset ran scripted playbooks at machine speed. This one was someone sitting at a keyboard.\n\n---\n\n## 20. Repository Structure\n\n```\ntpot-soc-automation/\n│\n├── LICENSE\n│\n├── README.md\n│\n├── config/\n│   ├── inputs.conf\n│   ├── claude_desktop_config.json\n│   └── n8n-compose.yaml\n│\n├── scripts/\n│   └── install-docker.sh\n│\n├── n8n-workflow/\n│   └── n8n-soc-workflow.json\n│\n├── detection-engineering/\n│   ├── splunk/\n│   │   └── queries.md\n│   │\n│   ├── suricata/\n│   │   ├── tpot_ruleset.xml\n│   │   ├── tpot-c2-alert-drop.rules\n│   │   └── tpot-ip-drop.rules\n│   │\n│   └── yara/\n│       └── tpot-rules.yar\n│\n├── report/\n│   ├── T-Pot_Threat_Intelligence_Report_v2.md\n│   └── T-Pot_Threat_Intelligence_Report_v2.pdf\n│\n├── queries/\n│   └── queries.md\n│\n├── iocs/\n│   ├── README.md\n│   ├── c2-urls.txt\n│   ├── dns-blocklist.txt\n│   ├── ip-blocklist.txt\n│   ├── malicious-md5-hashes-wannacry.txt\n│   ├── malicious-sha256-hashes.txt\n│   ├── mdrfckr-ssh-backdoor-key.txt\n│   └── undetected-hashes.txt\n│\n└── images/\n    └── media/\n        ├── *.png\n        ├── *.png\n        ├── *.png\n        └── ...\n```\n\n---\n\n## Notes\n\n-   The T-Pot MCP connection used in log analysis was through Claude.ai - the Splunk and Virustotal MCP servers were connected as tools in the conversation.\n-   All VirusTotal lookups were performed via MCP; the hash and IP results are documented in the full report.\n-   Log retention was adjusted to ~3 months partway through the deployment. Some early-window tool detections (Masscan, Mozi, libredtail-http) are documented from pre-rotation logs and are no longer queryable in the current Splunk index.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodelassey%2Ftpot-soc-automation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodelassey%2Ftpot-soc-automation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodelassey%2Ftpot-soc-automation/lists"}