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¶
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 transportserver=y- Act as debug server (waits for debugger connection)suspend=n- Don't wait for debugger to attach (usesuspend=yto 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¶
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.
Option A: Single Command (Recommended)¶
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:
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¶
- Open IntelliJ IDEA
- Go to Run → Edit Configurations
- Click + (Add) → Remote JVM Debug
- 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)
- Click Apply → OK
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¶
- Ensure SSH tunnel is running (from Step 2)
- Ensure Java application is running with debug enabled (from Step 1)
- In IntelliJ, click the Debug icon (bug icon)
- Select AWS Private EC2 Debug configuration
- You should see:
Connected to the target VM, address: 'localhost:5005', transport: 'socket' - Set breakpoints in your code
- Trigger the code path in your application
- 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):
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:
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?¶
Expected: Successfully logged in to NAT instance
2. Can NAT reach Private EC2?¶
Expected: Successfully logged in to private EC2
3. Is Java debug port open on EC2?¶
Expected: Shows Java process listening on port 5005
4. Can NAT reach debug port?¶
Expected: Connection successful
5. Is tunnel working on your laptop?¶
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:
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:
✓ 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:
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¶
Debug Checklist¶
- [ ] Java application running with
-agentlib:jdwpflag - [ ] Port 5005 listening on EC2 (verify with netstat)
- [ ] SSH tunnel running on laptop
- [ ]
telnet localhost 5005succeeds 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¶
- IntelliJ Remote Debug Documentation
- Java Platform Debugger Architecture
- SSH Port Forwarding Guide
- AWS Bastion Host Best Practices
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! 🐛🔍