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.
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.
IAM → Security recommendations → Enable MFA. Used an authenticator app, not SMS. If the console is compromised, MFA is the last line of defense.
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.
Billing → Budgets → $5/month threshold. All resources are Free Tier eligible — this is just an early warning for unexpected spin-up.
The honeypot lives in a completely separate VPC from any other AWS resources. Attacker traffic cannot laterally reach anything else in the account.
Name: honeypot-vpc · CIDR: 10.10.0.0/16
Name: honeypot-public · CIDR: 10.10.1.0/24
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.
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.
| Port | Protocol | Source | Purpose |
|---|---|---|---|
22 | TCP | Operator IP /32 | Real SSH — locked to one IP |
21 | TCP | 0.0.0.0/0 | Fake FTP |
23 | TCP | 0.0.0.0/0 | Fake Telnet |
80 | TCP | 0.0.0.0/0 | Fake HTTP |
2222 | TCP | 0.0.0.0/0 | Fake SSH |
3306 | TCP | 0.0.0.0/0 | Fake MySQL |
5900 | TCP | 0.0.0.0/0 | Fake VNC |
8080 | TCP | 0.0.0.0/0 | Fake HTTP-alt |
Instance: Amazon Linux 2023, t3.micro (Free Tier eligible), placed in honeypot-vpc with public IP enabled and honeypot-sg attached.
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
ssh -i ~/.ssh/honeypot-key.pem ec2-user@<public-ip>
ec2-user, not ubuntu. Using the wrong username causes a "Permission denied (publickey)" error even with a perfectly valid key.
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.
sudo yum update -y
sudo yum install -y python3-pip python3-devel libffi-devel openssl-devel gcc
# --ignore-installed setuptools needed on Amazon Linux
# because system setuptools is managed by RPM
sudo pip3 install twisted opencanary --ignore-installed setuptools
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.
sudo /usr/local/bin/opencanaryd --start
# Confirm all bait ports are listening
sudo ss -tlnp | grep -E '21|23|80|2222|3306'
# /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
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.
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.
{
"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"
}
]
}
}
}
}
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.
Table: honeypot-events · Partition key: src_ip · Sort key: timestamp
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}')
To avoid charges after the project completes, delete resources in this order. The VPC deletion automatically removes the subnet, route table, and security group.
Instances → select → Instance state → Terminate
Tables → honeypot-events → Delete
Log Groups → honeypot-logs → Actions → Delete log group
Functions → honeypot-log-parser → Actions → Delete
Detach and delete Internet Gateway, then delete honeypot-vpc
Delete honeypot-ec2-role and honeypot-lambda-role