Project Writeup  ·  Cybersecurity Lab  ·  2026

AWS Cloud Honeypot

Warren King  ·  University of Houston  ·  CIS / Cybersecurity
OpenCanary EC2 CloudWatch Lambda DynamoDB VPC Isolation Least Privilege IAM Threat Detection
Contents
  1. Architecture
  2. Phase 1 — Securing the AWS Account
  3. Phase 2 — Building an Isolated VPC
  4. Phase 3 — Security Groups
  5. Phase 4 — Launching the EC2 Instance
  6. Phase 5 — Installing & Configuring OpenCanary
  7. Phase 6 — Shipping Logs to CloudWatch
  8. Phase 7 — Lambda + DynamoDB
  9. Cleanup

Architecture

The honeypot is designed so the operator's personal machine never receives inbound attacker traffic. All hostile activity is contained entirely within AWS infrastructure in an isolated VPC. The operator only ever initiates outbound SSH connections to the instance.

Attacker (internet) │ ▼ EC2 Honeypot ← OpenCanary ports 21, 23, 80, 2222, 3306 │ ▼ CloudWatch Logs ──▶ Lambda ──▶ DynamoDB │ ▼ Dashboard
Key constraint: The personal machine never accepts inbound connections. SSH from the laptop is outbound-only — same as opening a browser tab.

Securing the AWS Account

Before touching any EC2 resources, the AWS account was hardened. A compromised honeypot instance could potentially be used as a pivot into the broader AWS environment — least privilege IAM and MFA contain that risk.

01.

Enable MFA on root

IAM → Security recommendations → Enable MFA. Used an authenticator app, not SMS. If the console is compromised, MFA is the last line of defense.

02.

Create a scoped IAM user

Created honeypot-admin with only the permissions this project needs: EC2, CloudWatch, DynamoDB, Lambda. No billing, no S3, no Route53. Blast radius contained if credentials leak.

03.

Set a billing alert

Billing → Budgets → $5/month threshold. All resources are Free Tier eligible — this is just an early warning for unexpected spin-up.


Building an Isolated VPC

The honeypot lives in a completely separate VPC from any other AWS resources. Attacker traffic cannot laterally reach anything else in the account.

01.

Create the VPC

Name: honeypot-vpc  ·  CIDR: 10.10.0.0/16

02.

Create a public subnet

Name: honeypot-public  ·  CIDR: 10.10.1.0/24

03.

Attach Internet Gateway and update route table

Created an Internet Gateway, attached it to the VPC, then added a route: 0.0.0.0/0 → igw. Without this, the instance has a public IP but no actual path to the internet — attackers couldn't reach the bait ports.


Security Groups

The security group is the critical piece that keeps the operator safe while exposing bait services. Port 22 is restricted to the operator's exact IP using a /32 CIDR — attackers hitting the instance from the internet cannot SSH in. OpenCanary intercepts everything else.

PortProtocolSourcePurpose
22TCPOperator IP /32Real SSH — locked to one IP
21TCP0.0.0.0/0Fake FTP
23TCP0.0.0.0/0Fake Telnet
80TCP0.0.0.0/0Fake HTTP
2222TCP0.0.0.0/0Fake SSH
3306TCP0.0.0.0/0Fake MySQL
5900TCP0.0.0.0/0Fake VNC
8080TCP0.0.0.0/0Fake HTTP-alt
Security group inbound rules
Security group inbound rules — port 22 locked to operator /32, bait ports open to world

Launching the EC2 Instance

Instance: Amazon Linux 2023, t3.micro (Free Tier eligible), placed in honeypot-vpc with public IP enabled and honeypot-sg attached.

Securing the key pair

The .pem file was downloaded during instance creation and immediately moved to ~/.ssh with locked permissions. AWS enforces this — SSH will refuse to connect if the key file is readable by anyone other than the owner.

# Move key to the standard ssh directory
cp "/c/Users/Warren King/Downloads/honeypot-key.pem" ~/.ssh/honeypot-key.pem

# Restrict to read-only by owner — required by SSH
chmod 400 ~/.ssh/honeypot-key.pem
Key pair in .ssh with correct permissions
~/.ssh/honeypot-key.pem with -r-------- permissions confirmed

Connecting

ssh -i ~/.ssh/honeypot-key.pem ec2-user@<public-ip>
Gotcha: Amazon Linux uses ec2-user, not ubuntu. Using the wrong username causes a "Permission denied (publickey)" error even with a perfectly valid key.

Installing & Configuring OpenCanary

OpenCanary is a lightweight honeypot daemon from Thinkst that simulates multiple network services and logs every connection attempt as structured JSON. It's the right tool here because it's purpose-built for this use case — low overhead, broad service coverage, clean log output.

Install dependencies

sudo yum update -y
sudo yum install -y python3-pip python3-devel libffi-devel openssl-devel gcc
System dependencies installed
System dependencies installed on Amazon Linux 2023

Install OpenCanary

# --ignore-installed setuptools needed on Amazon Linux
# because system setuptools is managed by RPM
sudo pip3 install twisted opencanary --ignore-installed setuptools

Config file

Created at /etc/opencanaryd/opencanary.conf — enables fake FTP, HTTP, MySQL, SSH, Telnet, and port scan detection, with all events written to a structured JSON log file.

OpenCanary config file
/etc/opencanaryd/opencanary.conf — services enabled with log output path

Start and verify

sudo /usr/local/bin/opencanaryd --start

# Confirm all bait ports are listening
sudo ss -tlnp | grep -E '21|23|80|2222|3306'
Bait ports listening via twistd
All fake services listening — twistd process bound to ports 21, 23, 80, 2222, 3306
Port scan confirming all services active
Port scan output confirming all five bait services active

systemd service for auto-restart

# /etc/systemd/system/opencanary.service
[Unit]
Description=OpenCanary Honeypot
After=network.target

[Service]
User=root
ExecStart=/usr/local/bin/opencanaryd --start
ExecStop=/usr/local/bin/opencanaryd --stop
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable opencanary

Shipping Logs to CloudWatch

Every OpenCanary event is a JSON object written to /var/log/opencanary/opencanary.log. The CloudWatch agent tails this file and ships new entries to AWS in real time.

IAM Role — no credentials on disk

Rather than placing AWS credentials on the instance directly, an IAM role honeypot-ec2-role was created with CloudWatchAgentServerPolicy and assigned to the EC2 instance. The instance receives automatically rotating temporary credentials through the instance metadata service. No keys on disk, ever.

CloudWatch Agent config

{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/opencanary/opencanary.log",
            "log_group_name": "honeypot-logs",
            "log_stream_name": "{instance_id}",
            "timezone": "UTC"
          }
        ]
      }
    }
  }
}
CloudWatch agent active and running
CloudWatch agent active — piping /var/log/opencanary/opencanary.log to honeypot-logs log group

Logs in CloudWatch Insights

honeypot-logs log group in CloudWatch
honeypot-logs log group created in CloudWatch Insights
Live honeypot events in CloudWatch
Live attacker events streaming into CloudWatch — 7 records captured within minutes of deployment
The instance was hit within minutes of going live. The internet continuously scans every public IP — bots will find open ports almost immediately.

Lambda + DynamoDB

A Lambda function subscribes to the honeypot-logs log group via a CloudWatch trigger. On every new batch of log events, it decompresses and parses each JSON entry, filters out internal startup messages, and writes structured attacker records to DynamoDB.

DynamoDB table

Table: honeypot-events  ·  Partition key: src_ip  ·  Sort key: timestamp

Lambda function (Python 3.12)

import json, boto3, base64, gzip

dynamodb = boto3.resource('dynamodb', region_name='us-east-2')
table = dynamodb.Table('honeypot-events')

def lambda_handler(event, context):
    cw_data = event['awslogs']['data']
    compressed = base64.b64decode(cw_data)
    uncompressed = gzip.decompress(compressed)
    log_data = json.loads(uncompressed)

    for log_event in log_data['logEvents']:
        try:
            message = json.loads(log_event['message'])
            src_ip = message.get('src_host', '')
            if not src_ip:
                continue
            table.put_item(Item={
                'src_ip':    src_ip,
                'timestamp': message.get('utc_time', ''),
                'dst_port':  str(message.get('dst_port', '')),
                'service':   get_service(str(message.get('dst_port', ''))),
                'logdata':   json.dumps(message.get('logdata', {}))
            })
        except Exception as e:
            print(f"Error: {e}")

def get_service(port):
    return {
        '21': 'FTP',  '23': 'Telnet', '80': 'HTTP',
        '2222': 'SSH', '3306': 'MySQL', '5900': 'VNC'
    }.get(port, f'Port-{port}')

Cleanup

To avoid charges after the project completes, delete resources in this order. The VPC deletion automatically removes the subnet, route table, and security group.

01.

EC2

Instances → select → Instance state → Terminate

02.

DynamoDB

Tables → honeypot-events → Delete

03.

CloudWatch

Log Groups → honeypot-logs → Actions → Delete log group

04.

Lambda

Functions → honeypot-log-parser → Actions → Delete

05.

VPC

Detach and delete Internet Gateway, then delete honeypot-vpc

06.

IAM

Delete honeypot-ec2-role and honeypot-lambda-role

Total cost: All resources used are Free Tier eligible. Estimated bill for this deployment: $0.