Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Cowrie and Grafana Honeypot using Terraform on DigitalOcean — Medium-interaction SSH/Telnet honeypot built with Cowrie, Loki, Promtail, and Grafana - provisioned on DigitalOcean via Terraform with a GitLab CI validation pipeline. | Kitploit
Tools/GitLabGitLab/oseguera12/cowrie-honeypot-digitalocean
Cloud Infrastructure SecurityNetwork SecurityDevSecOpsThreat IntelligenceLog Analysis
GitLaboseguera12/cowrie-honeypot-digitalocean

Cowrie and Grafana Honeypot using Terraform on DigitalOcean

Medium-interaction SSH/Telnet honeypot built with Cowrie, Loki, Promtail, and Grafana - provisioned on DigitalOcean via Terraform with a GitLab CI validation pipeline.

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository
Website
13 months agoNot yet reviewed

Cowrie & Grafana Honeypot using Terraform on DigitalOcean

Demo Video

Table of Contents

  • Table of Contents
  • System Overview
  • Technologies
  • Project Directory Structure
  • Hardware Requirements
  • Installation & Setup
  • Terraform State Management
  • CI/CD Pipeline
  • System Architecture
  • Observations
  • Usage
  • Project Documentation
    • Architecture Design and Trade-offs
    • System Execution Walkthrough
    • Issues & Limitations Encountered
    • Future Improvements
  • Security Considerations
  • Contributors
  • License

System Overview

An SSH/Telnet honeypot stack deployed on a DigitalOcean droplet using Terraform. Attacks are captured by Cowrie, stored in Loki, and visualised in Grafana with a live world map of attack origins.

Technologies

  • DigitalOcean
  • Terraform
  • Docker
  • Cowrie
  • Loki
  • Grafana
  • Promtail
  • DB-IP (City Lite download)

Project Directory Structure

root@kitploit:~
honeypot/
├── cowrie/
│   └── etc/
│       ├── cowrie.cfg                   # Cowrie honeypot configuration
│       └── userdb.txt                   # Accepted and rejected fake credentials
├── geoip/
│   └── .gitkeep                         # Placeholder - MMDB files are gitignored
├── grafana/
│   ├── provisioning/
│   │   ├── dashboards/
│   │   │   ├── dashboards.yml           # Provisioning: dashboard provider
│   │   │   └── honeypot-dashboard.json  # Pre-built Attack Monitor dashboard
│   │   └── datasources/
│   │       └── loki.yml                 # Auto-provisioned Loki datasource
│   └── grafana.ini                      # Grafana server settings
├── loki/
│   └── config.yml                       # Loki single-binary config and retention
├── promtail/
│   └── config.yml                       # Promtail scrape and GeoIP pipeline
├── scripts/
│   ├── geoip-update.sh                  # Download or refresh GeoIP database
│   └── setup-firewall.sh                # Host UFW rules for honeypot ports
├── terraform/
│   ├── templates/
│   │   ├── cloud-init.yaml.tftpl        # Droplet first-boot script (Terraform-templated)
│   │   └── env.tftpl                    # `.env` lines embedded via Terraform
│   ├── backend.tf                       # Terraform backend config
│   ├── main.tf                          # Droplet, SSH key, firewall, cloud-init
│   ├── outputs.tf                       # IPs and helpful post-apply values
│   ├── terraform.tfvars.example         # Example variable values (copy to terraform.tfvars)
│   ├── variables.tf                     # Terraform input variables
│   └── versions.tf                      # Terraform and provider version constraints
├── .env.example                         # Example environment file for manual setup
├── .gitignore                           # Ignored paths and files
├── .gitlab-ci.yml                       # CI/CD pipeline: fmt, validate, Checkov
├── docker-compose.yml                   # Docker Compose file for the honeypot stack
├── LICENSE                              # GPLv2 license
├── manual-deployment.sh                 # Legacy VM bootstrap without Terraform
└── README.md                            # Project documentation

Hardware Requirements

Note: These requirements are based on the DigitalOcean Basic Droplet plan as of May 2026 and are the minimum requirements to run the project.

  • Provider: DigitalOcean
  • Plan: Basic Droplet - 1 Intel vCPU
  • RAM: 1 GB (+2 GB swap)
  • Storage: 35 GB NVMe SSD
  • OS: Ubuntu 24.04 LTS

Installation & Setup

Note: After installation, it is recommended to open a new terminal and verify you can still SSH on ADMIN_SSH_PORT (Default: 2022) before closing your original session.

Terraform Deployment (Recommended)

  1. Create a DigitalOcean API token with read/write permissions:
  • Log into DigitalOcean and navigate to Account > API > Tokens > Generate New Token.
  • Name your token (e.g., "Cowrie Honeypot") and select "Full Access" permissions.
  • Click "Generate Token" and copy the token value to a secure location (you won't be able to see it again).
  1. Create an SSH key pair on your local machine and copy the public key path.

  2. Go to the terraform directory then copy and edit the variables file:

Note: do_token, ssh_public_key_path, and grafana_admin_password must be configured at minimum in terraform.tfvars before applying.

root@kitploit:~
cd terraform
cp terraform.tfvars.example terraform.tfvars
  1. Apply the configuration and start the deployment:

Note: Terraform must be installed on your local machine. Visit Terraform Installation Guide for instructions.

root@kitploit:~
terraform init  # Initialize Terraform and download providers
terraform apply
  1. If Testing - Redeploy with replacement:
root@kitploit:~
terraform apply -replace="digitalocean_droplet.honeypot" 
  1. Clean-up - Destroy the infrastructure when you're done:
root@kitploit:~
terraform destroy

Manual Deployment (Legacy)

  1. Create a DigitalOcean droplet with the above hardware requirements and your SSH key.

  2. SSH into the droplet or use the DigitalOcean web console and run the following commands:

root@kitploit:~
ssh root@<your-droplet-ip> # If using DigitalOcean web console, skip this command
git clone https://gitlab.com/Oseguera12/cowrie-honeypot-digitalocean.git /opt/honeypot
cd /opt/honeypot
cp .env.example .env
nano .env
bash manual-deployment.sh

Terraform State Management

Note: By default, Terraform writes state to terraform/terraform.tfstate on your local machine. This file contains sensitive output values (droplet IP, Grafana password, SSH key fingerprint) and must never be committed. .gitignore covers *.tfstate and *.tfstate.*.

Risks of local state:

  • Lost if the machine is lost or the file is deleted
  • Cannot be shared across team members
  • No locking — two concurrent apply runs can corrupt the file

For anything beyond a personal lab, switch to a remote backend. terraform/backend.tf contains a commented-out DigitalOcean Spaces configuration (S3-compatible).

To enable remote backend:

  1. Create a Spaces bucket in your DigitalOcean account

  2. Generate a Spaces access key under API > Spaces Keys

  3. Export the keys as environment variables (do not put them in terraform.tfvars):

    root@kitploit:~
    export AWS_ACCESS_KEY_ID=<spaces-access-key>
    export AWS_SECRET_ACCESS_KEY=<spaces-secret-key>
    
  4. Uncomment the backend "s3" block in terraform/backend.tf and fill in your bucket name and region endpoint.

  5. Run terraform init -migrate-state to move existing local state to Spaces.

CI/CD Pipeline

.gitlab-ci.yml runs three jobs on every push in a single validate stage:

Note: terraform:fmt and terraform:validate block the pipeline on failure. checkov:scan is set to allow_failure: true because some findings are intentional trade-offs. Suppress specific acceptable findings with inline # checkov:skip=CKXXX comments rather than disabling the job entirely.

Known accepted risks

CKV_DIO_4 ("Ensure the firewall ingress is not wide open") fires once against the entire digitalocean_firewall resource. It is suppressed with a single # checkov:skip=CKV_DIO_4: comment placed inside the resource block in terraform/main.tf. The table below documents why each open port is intentional.

System Architecture

root@kitploit:~
flowchart TB
  internet((Internet))

  subgraph tf["Terraform"]
    fw[DigitalOcean Cloud Firewall]
    droplet[Ubuntu 24.04 droplet]
    fw --> droplet
  end

  subgraph compose["Docker Compose on droplet"]
    cowrie["Cowrie honeypot — SSH on port 22, Telnet on port 23"]
    promtail[Promtail with GeoIP labels]
    loki[Loki log store]
    grafana["Grafana — host :3000"]
    cowrie -->|JSON logs from ./data/cowrie-logs| promtail --> loki --> grafana
  end

  internet -->|22/23 honeypot| fw
  internet -->|Admin SSH :2022| fw
  internet -->|Grafana :3000| fw
  droplet --> compose

Observations

Data captured over 5 days of live deployment (2026-05-05 to 2026-05-09):

Top attacking countries

Credential patterns

The most common username attempted was root with 3,866 attempts, followed by admin (728) and user (494), reflecting automated scanners targeting default credentials and known service accounts. The most common password was 123456 (1,480 attempts) followed by 123 and 12345, consistent with dictionary-based brute-force tooling.

Attacker behavior inside the fake shell

868 login attempts succeeded against the fake credential set. Of those sessions, 836 commands were executed. The most common command was uname -s -v -n -r -m (360 executions) — a standard system fingerprinting command run by automated post-exploitation scripts to identify the target OS and architecture before deploying a payload. Other observed commands included export HISTFILE=/dev/null to disable shell history logging, and export HISTSAVE=/dev/null, indicating attackers actively attempting to cover their tracks even inside what they believed was a compromised system.

Key takeaway

The volume of automated scanning traffic — 41,700 connections in 5 days from 106 countries — confirms that any publicly accessible SSH service faces constant brute-force attempts within hours of exposure. The behavioral pattern of immediately running uname followed by history-suppression commands is consistent with automated post-exploitation frameworks operating with minimal human involvement.

Usage

Using Grafana Dashboard

Note: Dashboard Credentials are set in terraform.tfvars or .env (for manual deployment)

Dashboard can be accessed at:

root@kitploit:~
http://<your-droplet-ip>:3000

Login with admin / <GRAFANA_ADMIN_PASSWORD>.

Cowrie honeypot accepted credentials

cowrie/etc/userdb.txt - contains the accepted credentials for the honeypot.

  • Edit this file to tune the honeypot credentials

  • Accepted logins drop the attacker into a fake shell where all commands are logged

  • Rejected entries are logged as failed attempts

Project Documentation

This section details the decisions made and issues encountered during the development of this project. It serves as a reflection of the project's development process and lessons learned. Skip to Security Considerations if you only want to know how to use the project.

Architecture Design and Trade-offs

Why Cloud Hosting?

  • Honeypots are designed to be highly available in order to capture and analyze data around the clock. While the honeypot could be deployed on a local machine using virtual machines or containers, keeping that machine online and accessible around the clock is not practical. Cloud hosting provides a more practical and reliable solution for hosting the honeypot and capturing data continuously.
  • Additionally, because cloud hosting lets you select specific resources and services, it is easier to create and maintain a codebase that can be deployed and expanded. Anyone interested in deploying their own honeypot can do so without extra steps, as long as the project works and the provisioned environment matches what the project expects.

Why DigitalOcean?

  • While other providers such as AWS, GCP, and Azure are available, DigitalOcean provides a more affordable option for this project. Because a honeypot must stay online and send data in and out of the droplet around the clock, DigitalOcean's pricing model helped keep costs predictable. This does not account for other providers' promotions, such as free credits or discounts for new users or long-term commitments.

Why Terraform?

  • The alternative, manual-deployment.sh, is for users who are not experienced with Terraform or prefer not to use it. That path requires provisioning a droplet, connecting to it, cloning the repo, and running the script. Terraform allows quicker iteration: edit the codebase and redeploy without repeating each manual step from your local machine. It also makes it easy to tear down infrastructure and start fresh. If the project grows from a single honeypot into a honeynet, Terraform scales more cleanly than manual provisioning or the legacy script alone.

Why Cowrie?

  • Cowrie is a well-known, actively maintained medium-interaction honeypot. It can use more resources than lighter traps, but its features and fit with the rest of the stack made it a natural choice. Other honeypots could be swapped in for specific use cases, though many are unmaintained or need even more resources.

Why Loki?

  • The main constraint was hardware: log aggregation and search on a 1 GB RAM droplet with several other services running. Elasticsearch was too heavy. Graylog still depends on Elasticsearch under the hood and would not run properly within the limits. Other options were similarly resource-hungry. Grafana Loki matched the stack and stayed within budget except under very heavy queries. Some query features were sacrificed: if dashboards pull too much data, Loki becomes slow, the UI feels unresponsive, and the honeypot can miss or delay data.

Why Grafana?

  • I originally considered a custom dashboard, but Grafana is complete, customizable, and well documented, which sped up setup so I could focus on collecting and shipping logs. It is not as lightweight as a minimal custom UI, but after a small droplet upgrade it runs comfortably within the hardware limits. Other tools with pre-built dashboards would have used more resources without clear gains for this use case.

Why Promtail?

  • Promtail is the established Loki log shipper (Grafana Alloy is the newer successor). Promtail is light on RAM and supports pipeline stages (JSON parsing, GeoIP, labels) that match this project. Alternatives such as Fluentd or Logstash were too heavy for a 1 GB droplet.

Why GeoIP?

  • Attack maps need city and country context from source IPs. The project uses DB-IP City Lite via scripts/geoip-update.sh: no account or API key, which keeps deployment simple for anyone cloning the repo. Promtail's built-in GeoIP stage reads the local MMDB file and adds labels for the Grafana Geomap panel.

System Execution Walkthrough

Step by step overview of what occurs when the honeypot is setup using Terraform or the manual deployment script.

Terraform (recommended)

On your machine:

  1. Configure terraform/terraform.tfvars: API token, SSH public key path, Grafana password, repo URL/branch, optional manage_do_firewall
  2. Run terraform init: installs the DigitalOcean provider locally
  3. Run terraform apply: Terraform builds a plan, then creates or updates resources

In DigitalOcean:

  1. SSH public key is uploaded to your account
  2. Droplet is created from your Terraform variables:
    • Image (Ubuntu), size, and region come from droplet_image, droplet_size, region, etc.
    • The SSH key from step 4 is attached
    • user_data is set to the rendered cloud-init: on first boot the VM gets the admin SSH port, git repo_url / repo_branch, and a base64-encoded copy of the generated .env (Grafana password and ADMIN_SSH_PORT from env.tftpl)
  3. Cloud firewall (only when manage_do_firewall is true):
    • Terraform creates a DigitalOcean firewall and associates it with this droplet
    • Inbound TCP: your admin SSH port, 22, 23, and 3000 (see terraform/main.tf)
    • Outbound: broad TCP/UDP and ICMP so the droplet can update packages, pull images, and download GeoIP data

On the droplet, cloud-init runs automatically:

  1. Cloud-config: package_update / package_upgrade, then install listed packages (curl, git, ufw, …). Early in boot, write_files creates /root/honeypot.env (Terraform-rendered env.tftpl as base64: Grafana password, ADMIN_SSH_PORT, etc.)
  2. runcmd bootstrap script (order in terraform/templates/cloud-init.yaml.tftpl): set iptables / ip6tables to legacy backends
  3. Swap: 2 GB file if missing (/swapfile, fstab, swappiness)

Inside Docker Compose:

  1. Loki starts and must pass its health check (/ready on port 3100 in-container; on the host only 127.0.0.1:3100)
  2. Promtail and Grafana wait for Loki to be healthy (depends_on in docker-compose.yml), then start
  3. Cowrie can start independently: it publishes host ports 22 and 23 into the container and writes JSON logs under data/cowrie-logs for Promtail

Manual Setup (legacy)

User creates the VM, SSH in, and runs the script from the cloned repo.

manual-deployment.sh does the following:

  1. Load and validate .env
  2. System update: apt update & apt upgrade
  3. Install packages: tools plus ufw and iptables
  4. Configure swap: 2 GB file if missing
  5. Install Docker: Engine + Compose plugin, enable services
  6. Configure SSH: sshd on ADMIN_SSH_PORT and disable ssh.socket
  7. Configure firewall: scripts/setup-firewall.sh
  8. Configure GeoIP: scripts/geoip-update.sh and monthly cron
  9. Start the stack: docker compose pull / up -d, then the script waits for Loki and Grafana health checks and prints a summary

Issues Encountered

Hardware Limitations

  • Limited hardware resources resulted in slow performance and occasional timeouts. This impacted the dashboard which depends on being able to query the database quickly in user set intervals sometimes by multiple devices accessing the dashboard at the same time.

Solutions Considered:

  • Scale vertically for more resources
  • Custom dashboard with fewer panels to reduce the load on the database
  • Limiting the number of devices that can access the dashboard at the same time
  • Aggregating logs in a separate database for the dashboard to read from to reduce strain on the main database

Resolution:

  • Scaled to a slightly larger droplet with better performance

Trade-offs:

  • While the cost increased around $1-2 per month, the resources available doubled. Access to NVMe SSD storage and more RAM over the Basic Droplet allowed a more stable and performant dashboard. This avoided having to spend time creating a custom dashboard with fewer panels and a less responsive experience.

Lessons Learned:

  • Getting as many resources as possible while staying within budget by taking advantage of promotions, discounts, and pricing tiers. The difference between the most basic droplet and a slight increase in cost resulted in doubling the resources available, which saved time by allowing a more complete product. Additionally, getting the honeypot running sooner meant capturing and analysing data sooner.

Timeout Issues

  • When attempting to SSH into the droplet for admin access, connections would time out. Separately, docker compose up failed because port 22 was already in use and could not be bound by Cowrie.

Solutions Considered:

  • sshd was misconfigured or listening on the wrong port
  • Firewall rules were blocking the admin SSH port after it was moved
  • Another process was already holding port 22

Resolution:

  • Corrected the order of operations: sshd is moved to ADMIN_SSH_PORT (default 2022) and restarted first, then ssh.socket is stopped and disabled to free port 22 for Docker to bind for Cowrie. UFW rules are applied after both steps to allow the new admin port.

Trade-offs:

  • No functional trade-offs. This was a configuration and ordering fix.

Lessons Learned:

  • It's important to be aware of any services which need specific resources freed up in a particular order before they can start. In this case on Ubuntu 24.04, ssh.socket is a systemd socket unit that keeps port 22 reserved for on-demand SSH activation. It must be stopped and disabled before Docker can bind host port 22 for Cowrie. If it's still active, docker compose up fails with "address already in use".

Future Improvements

GeoIP Functionality

  • City-level GeoIP is approximate. The map panels depend on the DB-IP database and may miss or mis-label some IPs (mobile, VPNs, stale data). The stack could be extended later with a different database or enrichment pipeline for higher confidence in geography.

Grafana HTTPS

  • Grafana currently runs on HTTP. A future improvement would be placing it behind a reverse proxy (nginx or Caddy) with a TLS certificate, which would also allow restricting the Grafana port to localhost and proxying through 443.

Proxy Server Functionality

  • To make the honeypot appear more legitimate, it could be proxied through a server in a different region. This would make it harder for attackers to identify it as a honeypot based on its IP or geolocation.

Security Considerations

  • Grafana uses HTTP. Port 3000 with no TLS means credentials transit in cleartext.
  • Loki listens only on 127.0.0.1 (not exposed)
  • terraform.tfvars is gitignored and must never be committed
  • Cowrie runs as an unprivileged user inside its container
  • Grafana should be placed behind a reverse proxy with HTTPS if exposed beyond a test environment

Contributors

  • Oseguera12

License

GNU General Public License Version 2.0 (GPLv2) See the full license text in the LICENSE file.

Download Tool
JobToolPurpose
terraform:fmthashicorp/terraform:1.8Formatting: fails if any file needs terraform fmt
terraform:validatehashicorp/terraform:1.8Config validity without contacting DigitalOcean
checkov:scanbridgecrew/checkov:latestIaC misconfigurations in the Terraform code
Check IDResourceFindingDecision
CKV_DIO_4digitalocean_firewall.honeypot — admin SSH inboundAdmin SSH port open to 0.0.0.0/0Accepted — restricting to a fixed IP is impractical for a portable lab; recommended in production
CKV_DIO_4digitalocean_firewall.honeypot — port 22 inboundPort 22 open to 0.0.0.0/0Intentional — this is the SSH honeypot surface; restricting the source defeats the purpose
CKV_DIO_4digitalocean_firewall.honeypot — port 23 inboundPort 23 open to 0.0.0.0/0Intentional — Telnet honeypot surface; same reasoning as port 22
CKV_DIO_4digitalocean_firewall.honeypot — port 3000 inboundGrafana HTTP exposed to 0.0.0.0/0Accepted for lab accessibility — known limitation documented in Security Considerations; production deployments should restrict to a known IP or proxy through HTTPS on port 443
MetricValue
Total connections41,700
Login attempts15,400
Successful logins868
Commands executed836
Files downloaded6
Unique source countries106
Country
Connections
Germany9,768
Netherlands9,110
United States7,706
United Kingdom3,858
Singapore1,770
Belgium1,422
PanelDescription
Attack World MapGeomap with heatmap + marker layers showing origin of every connection
Total ConnectionsCount of inbound honeypot sessions in the selected time range
Login AttemptsTotal credential brute-force attempts
Successful LoginsAttackers who matched userdb.txt credentials
Commands ExecutedShell commands executed inside the fake shell
Files DownloadedMalware/scripts captured via wget/curl
Connection RateTime series: connections/s, failed logins/s, successes/s
Top UsernamesMost-tried SSH usernames
Top PasswordsMost-tried passwords
Top CommandsMost-executed shell commands
Attacks by CountryCountry-level connection table
Recent EventsLive log stream of latest events
File DownloadsTable of every file an attacker tried to pull
  • Docker: install Engine and Compose plugin from Docker’s apt repo; systemctl enable --now docker
  • Real SSH: sshd on ADMIN_SSH_PORT; disable ssh.socket so host port 22 is free for Cowrie
  • Application repo: git clone from repo_url / repo_branch into /opt/honeypot (must include docker-compose.yml and configs)
  • .env on disk: move /root/honeypot.env to /opt/honeypot/.env (mode 600)
  • Host firewall: scripts/setup-firewall.sh (UFW for admin SSH, 22, 23, 3000)
  • GeoIP: scripts/geoip-update.sh (DB-IP City Lite into geoip/); append a monthly cron line for the same script
  • Cowrie bind mounts: create data/cowrie-logs and data/cowrie-dl with permissions Compose expects
  • Stack start: from /opt/honeypot, docker compose pull then docker compose up -d