Posted on

Table of Contents

Introduction

Looking for a fun and educational hands-on project that combines Rust coding with cloud deployment? In this guide, we will build a simple honeypot—a minimal TCP server in Rust that listens on a specific port and logs every incoming request—then deploy it to an Ubuntu instance on Oracle Cloud Infrastructure (OCI) for FREE. A honeypot offers a fascinating window into the malicious scans and attacks traveling the internet while also providing a playful way to practice coding, cross-compiling, and network configuration. By following these steps, you will gain valuable Rust experience, learn how to set up OCI for external traffic, and come away with deeper insight into real-world attack patterns.

  • Note 1: I am using a Apple M1 Pro MacBook for development and cross-compiling for x86_64 architecture.

1. Prerequisites

Welcome to the warm-up lap before our Rust honeypot grand adventure! Here’s what you need to get started:

1.1 Oracle Cloud Account

First things first: snag an Oracle Cloud account (free is our favorite price). The Always Free tier is pretty sweet since it gives you a no-cost VM to play around with. Perfect for deploying a honeypot without emptying your wallet.

  • Note 1: you can of course use any cloud provider or your own server for this project. But Oracle Cloud offers an "Always Free" tier that allows you to run a small VM at no cost and I was looking for an excuse to try it out. Initially I was interested in the ARM architecture but those are limited in my region of choice so I am using a AMD64 instance for this project.

1.2 Ubuntu VM in OCI

Go to the Oracle Cloud console and spin up an Ubuntu instance. Make sure you’ve got your SSH key set up and can actually get in. Some additional information on how to use SSH can be found in the Oracle Cloud documentation.

1.3 Rust Installed Locally

If you do not have Rust installed look here for the basic setup steps. To add another architecture Rust up your local environment with the following:

rustup update

# Add the target for x86_64 architecture
rustup target add x86_64-unknown-linux-gnu

Personally I am using stable-aarch64-apple-darwin on an Apple Silicon Mac for development and cross-compilation. Because I wanted to cross-compile to x86_64 the architecture of AMD64 (a usual suspect for cloud VMs), I installed this handy toolchain:

brew install messense/macos-cross-toolchains/x86_64-unknown-linux-gnu

If your setup is different, fear not—just make sure you have some way to compile your Rust code into a binary that your Ubuntu server recognizes.

1.4 Basic Terminal / SSH Skills

Brush up on your command line wizardry and SSH know-how. If you can SSH into your server, open files in your favorite terminal editor you’re set! If you are not there yet, no worries—there are plenty of tutorials out there to help you get up to speed, e.g. here.


2. Coding the Honeypot in Rust

2.1 Project Setup

To get started, we’ll create a fresh Rust project for our honeypot:

cargo new honeypot

This initializes a new folder named honeypot containing your basic Cargo scaffolding. For a minimal approach, we won’t require any external crates at this stage—plain Rust’s standard library has all we need for simple socket listening and logging. Below is an example Cargo.toml you might use:

[package]
name = "honeypot"
authors = ["Bob Peters [email protected]>"]
version = "0.1.0"
edition = "2021"

[dependencies]

That’s it, no dependencies listed because Rust’s standard library already includes the core networking (std::net) and concurrency (std::thread) features we need for this basic honeypot. Of course, if you decide to add extra functionality (like logging libraries, async runtimes, or protocol parsers), this is where you’d specify them under [dependencies].

Next, open src/main.rs, and you will find a bare-bones “Hello, world!” file. We will soon replace that with our honeypot logic.

2.2 Minimal TCP Server Code

Here’s a simple TCP server that listens on port 2222, for every connection a thread is spawned. Each thread logs incoming data to a file and stdout.

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn main() {
    let listener = TcpListener::bind("0.0.0.0:2222").expect("Could not bind to address");

    println!("Honeypot is Listening on port 2222");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| {
                    handle_client(stream);
                });
            }
            Err(e) => {
                eprintln!("Error: {}", e);
            }
        }
    }
}

fn handle_client(mut stream: TcpStream) {
    let peer_addr = match stream.peer_addr() {
        Ok(addr) => addr.to_string(),
        Err(_) => "Unknown".to_string(),
    };

    let mut buffer = [0; 1024];
    let now = get_epoch_time();

    println!("Received connection from {}", peer_addr);

    // Open a (new) file to log the request
    let filename = format!("/opt/honeypot/{}_{}.log", now, peer_addr);

    let mut file = std::fs::OpenOptions::new()
        .append(true)
        .create(true)
        .open(filename)
        .expect("Could not open file");

    loop {
        match stream.read(&mut buffer) {
            Ok(n) => {
                if n == 0 {
                    println!("Connection closed by {}", peer_addr);
                    break;
                }

                // Try if the request is a valid string
                // If it is a valid string, it will be printed
                // If it is binary data, it will be printed as a string
                let request = String::from_utf8_lossy(&buffer[..n]);
                println!("Received request from {}: {}", peer_addr, request);

                // Write the request to the file
                file.write_all(&buffer[..n])
                    .expect("Could not write to file");

                // Close the file
                file.sync_all().expect("Could not sync file");
            }
            Err(e) => {
                eprintln!("Error reading from {}: {}", peer_addr, e);
                break;
            }
        }
    }
}

fn get_epoch_time() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("Time went backwards")
        .as_secs()
}

2.2.1 Handling Connections

Each incoming connection is handled in a separate thread to avoid blocking the main loop. This way, the honeypot can handle multiple connections simultaneously. The handle_client function reads data from the incoming stream, logs it to a file, and prints it to the console. The get_epoch_time function generates a timestamp for the log file name.

The TcpListener is bound to 0.0.0.0:2222, which means it listens on all available network interfaces on port 2222. This allows the honeypot to accept connections from any IP address. The listener.incoming() method returns an iterator over incoming connections (infinitely), which we loop over to handle each connection. Great job! You’ve set up your first TCP listener.

The TcpStream represents the connection to a client. We use stream.peer_addr() to get the IP address of the client. If the address is successfully retrieved, we convert it to a string; otherwise, we use "Unknown". We then create a buffer to read data from the stream and a file to log the incoming data.

2.2.2 Logging Data

The handle_client function reads data from the stream into the buffer and writes it to the log file. If the read operation returns 0 bytes, the connection is closed, and the function breaks out of the loop. We use String::from_utf8_lossy to convert the buffer to a string, handling invalid UTF-8 sequences gracefully. This allows us to log binary data like �, as well as text.

2.3 Enhancing the Honeypot

This can be extended in many ways, for example, you could add a banner to mimic a real service, or you could add a database to store the logs. Adding more ports, or even emulating a specific protocol. But for now, this is a good starting point.


3. Building and Packaging

3.1 Native vs. Cross-Compilation

Since we want to deploy our honeypot on an Ubuntu AMD64 server, we need to compile our Rust code for the x86_64 architecture. If your development machine has the same architecture, you can compile natively cargo build --release. Otherwise, you’ll need to cross-compile for the target architecture. It is one of the benefits of Rust that it is easy to cross-compile.

In my case, I am using an Apple M1 Pro MacBook, so I need to cross-compile for x86_64. Here’s how you can do it:

cargo build --release --target x86_64-unknown-linux-gnu

This creates a binary optimized for the x86_64 architecture, which you can then transfer to your Ubuntu server. It is located in target/x86_64-unknown-linux-gnu/release/honeypot.

Why not use the debug build? The debug build includes additional symbols and debugging information, making the binary larger and slower. The release build is optimized for performance and size, making it more suitable for deployment.

3.2 Testing Locally

Before deploying to Oracle Cloud, you can test your honeypot locally. Build it and run it with cargo run --release the binary on your development machine and connect to it using telnet localhost 2222 or nc localhost 2222. You should see the connection logs in your terminal and a new log file created in the current directory.


4. Deploying to Oracle Cloud

4.1 Orcale Cloud Infrastructure Network Configuration

By default, Oracle Cloud blocks inbound traffic on custom ports. You need to create an Ingress Rule in the Security List or Network Security Group for your instance’s subnet. Example: Open port 2222 to 0.0.0.0/0 via TCP.

  • Go to the Oracle Cloud console and navigate to your instance
  • Open your instance’s VNIC details
  • Click on the Security Lists or Network Security Groups link
  • Add a new Ingress Rule to allow traffic on port 2222 from 0.0.0.0/0 this is not recommended for production, but for a honeypot it is fine. This notation is called CIDR and it means all IP addresses.
  • Save the changes and wait for them to take effect.

Additional tip is to close the actual SSH port on 22 after you have deployed and tested the honeypot service, so that attackers aren’t attempting to brute-force your "real" SSH port.

4.2 Uploading the Honeypot Binary

To upload the binary to the server, you can use scp or a deployment script. I choose to use a deployment script to automate the process, because I will be doing this multiple times.

#!/usr/bin/env bash

##############################################################################
# Configuration
##############################################################################

# Path to your SSH private key
SSH_KEY="$HOME/.ssh/id_ssh_key" # Change this to your private key

# SSH username and server IP
SERVER_USER="ubuntu"
SERVER_HOST="xxx.xxx.xxx.xxx" # Change this to your server's IP

# Local paths
LOCAL_BINARY="./target/x86_64-unknown-linux-gnu/release/honeypot"                  # Compiled honeypot binary
LOCAL_INSTALL_SCRIPT="./install_honeypot_service.sh"

# Remote locations
REMOTE_DIR="/home/ubuntu"                  # Temporary location for uploads
REMOTE_BINARY_PATH="${REMOTE_DIR}/honeypot"
REMOTE_INSTALL_SCRIPT="${REMOTE_DIR}/install_honeypot_service.sh"

##############################################################################
# 0. Build the binary
##############################################################################

echo "=== Building the honeypot binary... ==="
cargo build --release --target x86_64-unknown-linux-gnu
if [ $? -ne 0 ]; then
  echo "Error: Failed to build the honeypot binary."
  exit 1
fi

##############################################################################
# 1. Upload the new binary
##############################################################################

echo "=== Uploading the honeypot binary to ${SERVER_USER}@${SERVER_HOST}... ==="
scp -i "${SSH_KEY}" "${LOCAL_BINARY}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_BINARY_PATH}"
if [ $? -ne 0 ]; then
  echo "Error: Failed to upload the honeypot binary."
  exit 1
fi
echo "=== Successfully uploaded binary. ==="

##############################################################################
# 2. Upload the install script
##############################################################################

echo "=== Uploading install script to ${SERVER_USER}@${SERVER_HOST}... ==="
scp -i "${SSH_KEY}" "${LOCAL_INSTALL_SCRIPT}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_INSTALL_SCRIPT}"
if [ $? -ne 0 ]; then
  echo "Error: Failed to upload install script."
  exit 1
fi
echo "=== Successfully uploaded install script. ==="

##############################################################################
# 3. Run install script remotely
##############################################################################

echo "=== Running install script on the remote server... ==="
ssh -i "${SSH_KEY}" "${SERVER_USER}@${SERVER_HOST}" << EOF
  set -e  # Exit on any command error

  # Make sure script is executable
  chmod +x "${REMOTE_INSTALL_SCRIPT}"

  # Run it with sudo privileges (prompts if needed)
  sudo "${REMOTE_INSTALL_SCRIPT}"

  # Done
EOF

if [ $? -eq 0 ]; then
  echo "=== Deployment successful! ==="
else
  echo "=== Deployment encountered an error. ==="
  exit 1
fi

This script automates the process of building the binary, uploading it to the server, and running an installation script. You can customize it to fit your needs.

4.3 Installation Script

Create an installation script to set up the honeypot on the server. Here’s an example script that installs the honeypot as a systemd service. It’s idempotent, meaning you can run it multiple times without causing issues.

#!/usr/bin/env bash
#
# install_honeypot_service.sh
#
# Idempotent script to install/upgrade the 'honeypot' systemd service.
# Safe to run multiple times.
#
# Steps:
# 1. Create 'honeypot' user if missing
# 2. Create /opt/honeypot dir if missing
# 3. Copy local binary to /opt/honeypot if it differs
# 4. Create or update systemd unit file
# 5. Reload & restart service

set -euo pipefail

###############################################################################
# Configurable parameters
###############################################################################
SERVICE_NAME="honeypot"
HONEYPOT_USER="honeypot"
HONEYPOT_GROUP="honeypot"
INSTALL_DIR="/opt/honeypot"
BINARY_NAME="honeypot"              # The name of your compiled binary
LOCAL_BINARY_PATH="./honeypot"      # Where your compiled binary is locally
SYSTEMD_UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
PERMISSIONS="750"                   # Permissions for $INSTALL_DIR

###############################################################################
# 1. Create user and group if they do not exist
###############################################################################
echo ">> Checking if user '$HONEYPOT_USER' exists..."
if id "$HONEYPOT_USER" &>/dev/null; then
  echo "   User '$HONEYPOT_USER' already exists."
else
  echo "   Creating system user '$HONEYPOT_USER'..."
  sudo useradd -r -s /usr/sbin/nologin "$HONEYPOT_USER"
fi

###############################################################################
# 2. Create /opt/honeypot directory if missing
###############################################################################
echo ">> Ensuring $INSTALL_DIR directory exists..."
if [ ! -d "$INSTALL_DIR" ]; then
  echo "   Creating directory $INSTALL_DIR..."
  sudo mkdir -p "$INSTALL_DIR"
  sudo chown "$HONEYPOT_USER":"$HONEYPOT_GROUP" "$INSTALL_DIR"
  sudo chmod "$PERMISSIONS" "$INSTALL_DIR"
else
  echo "   Directory $INSTALL_DIR already exists."
  echo "   Ensuring correct ownership & permissions..."
  sudo chown "$HONEYPOT_USER":"$HONEYPOT_GROUP" "$INSTALL_DIR"
  sudo chmod "$PERMISSIONS" "$INSTALL_DIR"
fi

###############################################################################
# 3. Copy binary to /opt/honeypot if it differs
###############################################################################
echo ">> Checking if $BINARY_NAME needs to be updated..."
LOCAL_CHECKSUM=$(sha256sum "$LOCAL_BINARY_PATH" | awk '{print $1}')
REMOTE_PATH="$INSTALL_DIR/$BINARY_NAME"

if [ -f "$REMOTE_PATH" ]; then
  REMOTE_CHECKSUM=$(sudo sha256sum "$REMOTE_PATH" | awk '{print $1}')
  if [ "$LOCAL_CHECKSUM" = "$REMOTE_CHECKSUM" ]; then
    echo "   Binary already matches the existing one; no copy needed."
  else
    # Verify if the service is already running
    # If it is, we need to stop it first
    if sudo systemctl is-active --quiet "$SERVICE_NAME.service"; then
        echo ">> Stopping $SERVICE_NAME service..."
        sudo systemctl stop "$SERVICE_NAME.service"
    fi

    echo "   Copying updated binary to $REMOTE_PATH..."
    sudo cp "$LOCAL_BINARY_PATH" "$REMOTE_PATH"
    sudo chown "$HONEYPOT_USER":"$HONEYPOT_GROUP" "$REMOTE_PATH"
    sudo chmod +x "$REMOTE_PATH"
  fi
else
  # Verify if the service is already running
  # If it is, we need to stop it first
  if sudo systemctl is-active --quiet "$SERVICE_NAME.service"; then
    echo ">> Stopping $SERVICE_NAME service..."
    sudo systemctl stop "$SERVICE_NAME.service"
  fi

  echo "   No existing binary at $REMOTE_PATH, copying now..."
  sudo cp "$LOCAL_BINARY_PATH" "$REMOTE_PATH"
  sudo chown "$HONEYPOT_USER":"$HONEYPOT_GROUP" "$REMOTE_PATH"
  sudo chmod +x "$REMOTE_PATH"
fi

###############################################################################
# 4. Create or update systemd service file
###############################################################################
echo ">> Installing or updating systemd unit file at $SYSTEMD_UNIT_PATH..."
SERVICE_FILE_CONTENT="[Unit]
Description=Honeypot Service
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/honeypot
User=${HONEYPOT_USER}
Group=${HONEYPOT_GROUP}
ExecStart=${REMOTE_PATH}
StandardOutput=file:/var/log/honeypot.log
StandardError=file:/var/log/honeypot_err.log
Restart=on-failure

[Install]
WantedBy=multi-user.target
"

# If the file doesn't exist OR the content differs, overwrite it
if [ -f "$SYSTEMD_UNIT_PATH" ]; then
  CURRENT_CONTENT=$(sudo cat "$SYSTEMD_UNIT_PATH")
  if [ "$CURRENT_CONTENT" != "$SERVICE_FILE_CONTENT" ]; then
    echo "   Updating systemd service file because content changed..."
    echo "$SERVICE_FILE_CONTENT" | sudo tee "$SYSTEMD_UNIT_PATH" >/dev/null
  else
    echo "   Systemd unit file already up-to-date."
  fi
else
  echo "   Creating new systemd service file..."
  echo "$SERVICE_FILE_CONTENT" | sudo tee "$SYSTEMD_UNIT_PATH" >/dev/null
fi

###############################################################################
# 5. Reload & restart systemd service
###############################################################################
echo ">> Reloading systemd daemon..."
sudo systemctl daemon-reload

echo ">> Enabling $SERVICE_NAME service to start on boot..."
sudo systemctl enable "$SERVICE_NAME.service"

echo ">> Restarting $SERVICE_NAME service..."
sudo systemctl restart "$SERVICE_NAME.service"

echo ">> Checking $SERVICE_NAME status..."
sudo systemctl status "$SERVICE_NAME.service" --no-pager || true

echo ">> Done! The $SERVICE_NAME service is set up and running."

This script installs the honeypot binary, creates a systemd service, and starts the service. It also ensures that the service is stopped before updating the binary or service file. The service logs are written to /var/log/honeypot.log and /var/log/honeypot_err.log.

The binary is run under the privileges of the honeypot user, which is created if it doesn’t exist. The honeypot binary is copied to /opt/honeypot, and the systemd service file is created or updated. The service is then reloaded, enabled, and restarted.

A pro-tip I got from Daniel Thompson-Yvetot was to assume the box will be powned and to make it more interesting for the attacker. For example, you change the name of the honeypot sevice to fastxmrminer.service and the binary to fastxmrminer and the attacker will think they hit the jackpot.

In the current version I kept it simple and straightforward for educational purposes, but you can expand on this idea.


5. Testing and Monitoring

5.1 Live Logs

You can monitor the honeypot logs in real-time using tail -f. Here’s how to check the logs:

  • SSH into your server.
  • Use tail -f /var/log/honeypot.log to monitor incoming requests.
  • Use tail -f /var/log/honeypot_err.log to monitor errors.

I separated the logs to make it easier to distinguish between regular logs and errors. You can customize the log paths and contents to suit your needs.

The stderr could benefit from structured logging or tracing, but for this simple honeypot, we’ll keep it basic.

5.2 Simulating an Attack

You can test your honeypot by connecting to it using telnet or nc from your local machine. Here’s how:

  • From your local machine, run telnet <server_ip> 2222 or nc <server_ip> 2222 to connect to the honeypot.
  • Enter some random text and press Enter.
  • Check the logs on the server to see the incoming request.

5.3 Gathering Intelligence

Over time, your honeypot will encounter a variety of malicious activities, including port scans, SSH brute-force attempts, and automated exploit scripts. By storing these logs for analysis, you can gain valuable insights into who’s probing your system and what they’re attempting to achieve.

Setting up alerts or notifications can help you identify specific patterns in real-time. Tools like the ELK Stack or Splunk are excellent for log analysis and can be configured to trigger alerts based on defined criteria.

For example, I observed numerous SSH brute-force attempts and port scans targeting my honeypot. It was fascinating to watch the diversity of attacks unfold in real time. One notable instance involved a “Makiko” client—a Rust-based SSH client—attempting to connect, showcasing that Rust is also being utilized for malicious purposes. Shortly after, I detected a Go-based client attempting to connect as well—although, admittedly, Go might not be the ideal language for this kind of job :)


Conclusion

Deploying a custom Rust honeypot on an always-free Oracle Cloud instance gives you hands-on insight into real-world malicious activity. You’ve learned how to code a simple TCP server in Rust, configure Oracle’s networking, manage an OCI's firewall, and centralize logging. With a bit of creativity, you can expand your honeypot to emulate common protocols, capture richer data, or integrate with threat intelligence pipelines. Happy hacking—safely, of course!


GitHub Repository

You can find the code for this project on my GitHub repository Rust Honeypot. Feel free to fork, modify, and experiment with it and if you star it - I will be very happy - I may be tempted to write more guides like this one it has been fun to write.

Author

Bob Peters - Rust Trends - LinkedIn

Further Reading

*Thank you for the tip Dario Lencina Talarico