Skip to content

Remote Debugging Java Applications on AWS EC2 Through SSH Tunneling

Overview

This guide explains how to set up remote debugging from IntelliJ IDEA on your local laptop to a Java application running on a private AWS EC2 instance accessible only through a NAT/Jump host.

Architecture

[Your Laptop] --SSH--> [NAT/Jump Host] --SSH--> [Private EC2] 
   :5005                  (public IP)            :5005 (debug port)

Traffic flow:
1. IntelliJ connects to localhost:5005 on your laptop
2. SSH tunnel forwards to NAT instance
3. NAT forwards to private EC2:5005
4. Java application receives debug connection

Prerequisites

On Your Laptop

  • Private key file (e.g., my-key.pem)
  • SSH client (built into macOS/Linux, or PuTTY for Windows)
  • IntelliJ IDEA installed

On NAT/Jump Host

  • SSH access from your laptop
  • SSH access to private EC2 instance
  • Public IP address

On Private EC2

  • Java application installed
  • Network connectivity from NAT instance

Step 1: Configure Java Application for Debugging

SSH to Private EC2

First, SSH into your private EC2 instance through the jump host:

# Option 1: Single command with ProxyJump
ssh -i my-key.pem -J ec2-user@<NAT-PUBLIC-IP> ec2-user@<PRIVATE-EC2-IP>

# Option 2: Two-step approach
# Step 1: SSH to NAT
ssh -i my-key.pem ec2-user@<NAT-PUBLIC-IP>

# Step 2: From NAT, SSH to private EC2
ssh ec2-user@<PRIVATE-EC2-IP>

Start Application with Debug Enabled

Once connected to the private EC2 instance:

For Standalone Java Application

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar your-application.jar

For Jetty Server

export JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
java -jar start.jar

Parameter Explanation

  • transport=dt_socket - Use socket transport
  • server=y - Act as debug server (waits for debugger connection)
  • suspend=n - Don't wait for debugger to attach (use suspend=y to debug startup)
  • address=*:5005 - Listen on all interfaces on port 5005
  • Java 9+: Use *:5005
  • Java 8: Use just 5005

Verify Debug Port is Listening

netstat -tulpn | grep 5005
# Expected output: tcp6  0  0  :::5005  :::*  LISTEN  <pid>/java

Step 2: Create SSH Tunnel

The SSH tunnel creates a secure connection from your laptop through the NAT to the private EC2's debug port.

Run this command from your laptop terminal:

ssh -i my-key.pem \
    -L 5005:localhost:5005 \
    -J ec2-user@<NAT-PUBLIC-IP> \
    ec2-user@<PRIVATE-EC2-IP> \
    -N

Parameter Explanation: - -i my-key.pem - Private key file - -L 5005:localhost:5005 - Forward local port 5005 to remote's localhost:5005 - -J ec2-user@<NAT-PUBLIC-IP> - Use NAT as jump/bastion host - ec2-user@<PRIVATE-EC2-IP> - Final destination (private EC2) - -N - Don't execute commands, just maintain tunnel

Keep this terminal window open while debugging!

Option B: With Keep-Alive (Prevents Disconnection)

ssh -i my-key.pem \
    -o ServerAliveInterval=60 \
    -o ServerAliveCountMax=3 \
    -L 5005:localhost:5005 \
    -J ec2-user@<NAT-PUBLIC-IP> \
    ec2-user@<PRIVATE-EC2-IP> \
    -N

Option C: Using SSH Config File (Best for Regular Use)

Create or edit ~/.ssh/config on your laptop:

# NAT/Jump Host
Host aws-nat
    HostName <NAT-PUBLIC-IP>
    User ec2-user
    IdentityFile ~/.ssh/my-key.pem
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Private EC2
Host aws-private-ec2
    HostName <PRIVATE-EC2-IP>
    User ec2-user
    ProxyJump aws-nat
    IdentityFile ~/.ssh/my-key.pem
    LocalForward 5005 localhost:5005

Then simply run:

ssh -N aws-private-ec2

Verify Tunnel is Working

From your laptop (in a different terminal):

# Check if port is listening locally
netstat -an | grep 5005
# Expected: tcp4  0  0  127.0.0.1.5005  *.*  LISTEN

# Test connection
telnet localhost 5005
# Should connect successfully

Step 3: Configure IntelliJ IDEA

Create Remote Debug Configuration

  1. Open IntelliJ IDEA
  2. Go to RunEdit Configurations
  3. Click + (Add) → Remote JVM Debug
  4. Configure the settings:
Name: AWS Private EC2 Debug
Debugger mode: Attach to remote JVM
Host: localhost
Port: 5005
Use module classpath: [Select your project module]
Search sources using module's classpath: ✓ (checked)
  1. Click ApplyOK

Configuration Screenshot Reference

Your configuration should look like: - Transport: Socket - Host: localhost - Port: 5005 - Command line arguments for remote JVM: (displayed for reference)


Step 4: Start Debugging

  1. Ensure SSH tunnel is running (from Step 2)
  2. Ensure Java application is running with debug enabled (from Step 1)
  3. In IntelliJ, click the Debug icon (bug icon)
  4. Select AWS Private EC2 Debug configuration
  5. You should see: Connected to the target VM, address: 'localhost:5005', transport: 'socket'
  6. Set breakpoints in your code
  7. Trigger the code path in your application
  8. Debug as normal!

Complete Example with Real Values

Let's say you have: - NAT Public IP: 54.123.45.67 - Private EC2 IP: 10.0.1.50 - Key file: ~/aws-key.pem

Commands to Run

Terminal 1 (Create SSH Tunnel):

ssh -i ~/aws-key.pem \
    -o ServerAliveInterval=60 \
    -L 5005:localhost:5005 \
    -J ec2-user@54.123.45.67 \
    ec2-user@10.0.1.50 \
    -N

Terminal 2 (Verify Tunnel):

telnet localhost 5005

IntelliJ Configuration: - Host: localhost - Port: 5005


Helper Scripts

Automated Tunnel Script

Create debug-tunnel.sh on your laptop:

#!/bin/bash

# Configuration
NAT_IP="54.123.45.67"
EC2_IP="10.0.1.50"
KEY_FILE="$HOME/aws-key.pem"
LOCAL_PORT=5005
REMOTE_PORT=5005

# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${GREEN}Starting debug tunnel...${NC}"
echo "NAT: $NAT_IP"
echo "EC2: $EC2_IP"
echo "Forwarding localhost:$LOCAL_PORT -> $EC2_IP:$REMOTE_PORT"
echo ""
echo -e "${YELLOW}Keep this terminal open while debugging${NC}"
echo "Press Ctrl+C to stop the tunnel"
echo ""

# Start tunnel
ssh -i "$KEY_FILE" \
    -o ServerAliveInterval=60 \
    -o ServerAliveCountMax=3 \
    -L $LOCAL_PORT:localhost:$REMOTE_PORT \
    -J ec2-user@$NAT_IP \
    ec2-user@$EC2_IP \
    -N

Make it executable:

chmod +x debug-tunnel.sh
./debug-tunnel.sh

Quick Start Script for Java Application

Create start-debug.sh on your EC2 instance:

#!/bin/bash

APP_JAR="your-application.jar"
DEBUG_PORT=5005

echo "Starting application with remote debugging on port $DEBUG_PORT"

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:$DEBUG_PORT \
     -jar $APP_JAR

Troubleshooting

Connection Tests

Run these tests in order to isolate issues:

1. Can you SSH to NAT?

ssh -i ~/aws-key.pem ec2-user@<NAT-PUBLIC-IP>

Expected: Successfully logged in to NAT instance

2. Can NAT reach Private EC2?

# While logged into NAT
ssh ec2-user@<PRIVATE-EC2-IP>

Expected: Successfully logged in to private EC2

3. Is Java debug port open on EC2?

# While logged into private EC2
netstat -tulpn | grep 5005

Expected: Shows Java process listening on port 5005

4. Can NAT reach debug port?

# While logged into NAT
telnet <PRIVATE-EC2-IP> 5005

Expected: Connection successful

5. Is tunnel working on your laptop?

# From your laptop (while tunnel is running)
netstat -an | grep 5005
telnet localhost 5005

Expected: Shows listening port and successful telnet connection

Common Issues and Solutions

Issue: "Connection refused" when starting tunnel

Cause: NAT cannot reach private EC2 or debug port isn't open

Solution:

# Test from NAT to EC2
ssh -i ~/aws-key.pem ec2-user@<NAT-PUBLIC-IP> "telnet <PRIVATE-EC2-IP> 5005"

# Verify Java is listening
ssh -i ~/aws-key.pem -J ec2-user@<NAT-PUBLIC-IP> ec2-user@<PRIVATE-EC2-IP> "netstat -tulpn | grep 5005"

Issue: "Permission denied (publickey)"

Cause: Key file permissions or key not found

Solution:

# Fix permissions
chmod 400 ~/aws-key.pem

# Verify key path
ls -l ~/aws-key.pem

# Add key to ssh-agent
eval $(ssh-agent)
ssh-add ~/aws-key.pem
ssh-add -l  # List added keys

Issue: NAT can't SSH to private EC2

Solution 1 - Copy key to NAT:

scp -i ~/aws-key.pem ~/aws-key.pem ec2-user@<NAT-PUBLIC-IP>:~/.ssh/

Solution 2 - Use SSH agent forwarding:

ssh -A -i ~/aws-key.pem ec2-user@<NAT-PUBLIC-IP>
# Now from NAT you can SSH to private EC2
ssh ec2-user@<PRIVATE-EC2-IP>

Issue: Tunnel keeps disconnecting

Solution: Add keep-alive options:

ssh -i ~/aws-key.pem \
    -o ServerAliveInterval=60 \
    -o ServerAliveCountMax=3 \
    -o TCPKeepAlive=yes \
    -L 5005:localhost:5005 \
    -J ec2-user@<NAT-PUBLIC-IP> \
    ec2-user@<PRIVATE-EC2-IP> \
    -N

Issue: "Unable to open debugger port" in Java

Cause: Incorrect address format for your Java version

Solution: - Java 9+: Use address=*:5005 - Java 8: Use address=5005

Issue: IntelliJ says "Connection refused"

Checklist: 1. ✓ SSH tunnel is running? 2. ✓ Java application is running with debug flags? 3. ✓ Using localhost and correct port in IntelliJ? 4. ✓ No firewall blocking localhost:5005 on your laptop?

Test:

# Verify tunnel
telnet localhost 5005
# Should connect

# Check what's listening
lsof -i :5005  # macOS/Linux
netstat -an | grep 5005  # All platforms

Issue: Breakpoints not working

Possible causes: 1. Source code doesn't match deployed code 2. Code was optimized during compilation 3. Wrong module classpath selected

Solution: - Ensure deployed JAR matches your source code - Rebuild and redeploy - Check module classpath in debug configuration - Enable "Search sources using module's classpath"


Security Best Practices

1. Never Expose Debug Port Directly

DON'T DO THIS:

# Opening debug port in security group
Allow TCP 5005 from 0.0.0.0/0

DO THIS INSTEAD: - Always use SSH tunneling - No security group changes needed - All traffic encrypted through SSH

2. Disable Debug in Production

Always remove debug flags when not actively debugging:

# ❌ Don't leave this running in production
java -agentlib:jdwp=... -jar app.jar

# ✓ Normal production startup
java -jar app.jar

Performance impact: Debug mode can significantly slow down your application.

3. Use SSH Config for Key Management

Instead of typing keys in commands:

# ❌ Keys visible in command history
ssh -i ~/my-secret-key.pem ...

# ✓ Keys managed in config file
# Add to ~/.ssh/config:
IdentityFile ~/.ssh/my-key.pem

4. Restrict NAT Security Group

Only allow SSH from your IP:

Type: SSH
Port: 22
Source: <YOUR-IP>/32
Description: My laptop only

5. Use Temporary Sessions

# Set tunnel to auto-close after inactivity
ssh -o ConnectTimeout=30 \
    -o ServerAliveInterval=60 \
    -o ServerAliveCountMax=3 \
    ...

6. Rotate Keys Regularly

  • Change EC2 key pairs periodically
  • Use AWS Systems Manager Session Manager as alternative
  • Enable CloudTrail for SSH access logging

7. Use Bastion Host Best Practices

  • Keep bastion minimal (no unnecessary software)
  • Enable logging and monitoring
  • Use MFA for bastion access
  • Consider AWS Systems Manager instead of traditional bastion

Quick Reference Card

Common Commands

# Start SSH tunnel (keep running)
ssh -i key.pem -L 5005:localhost:5005 -J user@NAT-IP user@EC2-IP -N

# Start Java with debug
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar

# Test tunnel
telnet localhost 5005

# Check listening ports
netstat -tulpn | grep 5005

# Fix key permissions
chmod 400 key.pem

IntelliJ Settings

Host: localhost
Port: 5005
Transport: Socket
Debugger mode: Attach to remote JVM

Debug Checklist

  • [ ] Java application running with -agentlib:jdwp flag
  • [ ] Port 5005 listening on EC2 (verify with netstat)
  • [ ] SSH tunnel running on laptop
  • [ ] telnet localhost 5005 succeeds from laptop
  • [ ] IntelliJ debug configuration set to localhost:5005
  • [ ] Breakpoints set in code
  • [ ] Source code matches deployed code

Alternative: AWS Systems Manager Session Manager

For environments that don't allow SSH, consider using AWS Systems Manager Session Manager with port forwarding:

# Install Session Manager plugin first
aws ssm start-session \
    --target <INSTANCE-ID> \
    --document-name AWS-StartPortForwardingSession \
    --parameters "portNumber=5005,localPortNumber=5005"

Advantages: - No SSH keys needed - No bastion/jump host required - Fully logged and audited - No open inbound ports

Requirements: - SSM Agent installed on EC2 - Appropriate IAM roles - AWS CLI configured


Additional Resources


Conclusion

SSH tunneling provides a secure, encrypted method to debug Java applications running on private EC2 instances without exposing debug ports to the internet. By following this guide, you can efficiently debug production issues while maintaining security best practices.

Remember: - Always use SSH tunnels, never expose debug ports - Remove debug flags when not actively debugging - Keep tunnel terminal open during debugging sessions - Verify connectivity at each step when troubleshooting

Happy debugging! 🐛🔍