Skip to main content

Tunnel Server using Docker and AWS

This example demonstrates the essence of:

  • an AWS RDS MySQL server that is not publicly accessible
  • an AWS ECS Fargate Task ssh-tunnel server that is publicly accessible
  • running a local docker ssh-tunnel client
  • Using MySQL client (via docker) to connect to the local ssh-tunnel client and communicate as if it was the MySQL server

It relies on:

  • "default" security group allowing all communication between each other
  • functional aws-cli and docker on the local machine

When the script is finished it will leave the RDS instance available for testing with the Secure Tunnel on CloudFlow guide.

Script

#!/bin/bash

# Produces REMOTE_SERVICE_ADDRESS
create_database(){
local db_instance_name="demo-privatedb-$(date +%s)"

aws rds create-db-instance \
--db-instance-identifier "${db_instance_name}" \
--db-instance-class db.t4g.micro \
--engine mysql \
--master-username masterusername \
--master-user-password masteruserpassword \
--allocated-storage 20 \
--no-publicly-accessible \
>/dev/null

aws rds wait db-instance-available \
--db-instance-identifier "${db_instance_name}"

REMOTE_SERVICE_ADDRESS=$(aws rds describe-db-instances \
--db-instance-identifier "${db_instance_name}" \
--query "DBInstances[0].Endpoint.Address" \
--output text)
export REMOTE_SERVICE_ADDRESS
echo "Exported REMOTE_SERVICE_ADDRESS=${REMOTE_SERVICE_ADDRESS}"
}

# Produces TUNNEL_CLIENT_KEY, TUNNEL_SERVER_KEY
generate_tunnel_keys() {
while read -r line; do
[ "${line}" == "SERVER (PUBLIC) KEY:" ] && read -r TUNNEL_SERVER_KEY
[ "${line}" == "CLIENT (PRIVATE) KEY:" ] && read -r TUNNEL_CLIENT_KEY
done < <(docker run --rm ghcr.io/section/section-secure-tunnel:sha-05d9f6a keygen)
export TUNNEL_CLIENT_KEY
export TUNNEL_SERVER_KEY
echo "Exported TUNNEL_CLIENT_KEY, TUNNEL_SERVER_KEY"
}

# Consumes TUNNEL_SERVER_KEY
# Produces TUNNEL_ADDRESS
setup_remote_server(){
local cluster_name=default
local tunnel_server_service_name="demo-ssh-tunnel-$(date +%s)"

# ensure cluster
aws ecs create-cluster \
--cluster-name "${cluster_name}" \
>/dev/null

# crete task definition
local task_definition_arn=$(aws ecs register-task-definition \
--requires-compatibilities FARGATE \
--family "${tunnel_server_service_name}" \
--network-mode awsvpc \
--cpu=256 \
--memory=512 \
--container-definitions "[{\"name\":\"ssh-tunnel\",\"image\":\"ghcr.io/section/section-secure-tunnel:sha-05d9f6a\",\"cpu\":256,\"command\":[\"server\",\"key\",\"remote_user_name\",\"${TUNNEL_SERVER_KEY}\",\"any\"],\"memory\":512,\"essential\":true}]" \
--query "taskDefinition.taskDefinitionArn" \
--output text \
)

# create security group
local group_id=$(aws ec2 create-security-group \
--group-name "${tunnel_server_service_name}" \
--description "ssh-tunnel inbound ${tunnel_server_service_name}" \
--query "GroupId" \
--output text)

# allow all to port 2022
aws ec2 authorize-security-group-ingress \
--group-id "${group_id}" \
--protocol tcp \
--port 2022 \
--cidr "0.0.0.0/0" \
>/dev/null

# allow to talk to default SG

# get all subnets
local allsubnet=$(aws ec2 describe-subnets \
--query "join(',',Subnets[].SubnetId)" \
--output text \
)

# default_group_id
local default_group_id=$(aws ec2 describe-security-groups \
--group-names default \
--query "SecurityGroups[0].GroupId" \
--output text \
)

# create service
aws ecs create-service \
--no-cli-pager \
--service-name "${tunnel_server_service_name}" \
--task-definition "${task_definition_arn}" \
--desired-count 1 \
--launch-type "FARGATE" \
--network-configuration "awsvpcConfiguration={subnets=[${allsubnet}],assignPublicIp=ENABLED,securityGroups=[${default_group_id},${group_id}]}" \
> /dev/null

# wait for service to start
aws ecs wait services-stable --services "${tunnel_server_service_name}"

# get the task
local taskArn=$(aws ecs list-tasks \
--service-name "${tunnel_server_service_name}" \
--query 'taskArns[0]' \
--output text)

# get the network interface
local networkInterfaceId=$(aws ecs describe-tasks \
--tasks "${taskArn}" \
--query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" \
--output text)

# get the public IP
TUNNEL_ADDRESS=$(aws ec2 describe-network-interfaces \
--network-interface-ids ${networkInterfaceId} \
--query "NetworkInterfaces[0].Association.PublicIp" \
--output text)

export TUNNEL_ADDRESS
echo "Exported TUNNEL_ADDRESS: ${TUNNEL_ADDRESS}"
}

# Consumes TUNNEL_ADDRESS, TUNNEL_CLIENT_KEY, REMOTE_SERVICE_ADDRESS
test_local_client(){
# start local ssh-tunnel client
docker run \
--name ssh-tunnel-client \
-d --restart unless-stopped \
-e TUNNEL_CLIENT_KEY \
ghcr.io/section/section-secure-tunnel:sha-05d9f6a \
client \
"${TUNNEL_ADDRESS}" \
key \
remote_user_name \
'${TUNNEL_CLIENT_KEY}' \
"3306:${REMOTE_SERVICE_ADDRESS}:3306" \
> /dev/null

# get IP address of client
local client_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ssh-tunnel-client)

# give tunnel chance to connect
sleep 1

# Connect and use!
echo -n "Talking to remote db via tunnel to get server version: "
MYSQL_PWD=masteruserpassword docker run -it --rm -e MYSQL_PWD --entrypoint "/bin/sh" mysql:8.0 "-c" "/usr/bin/mysql --silent --host=${client_ip} --user=masterusername --execute='SELECT VERSION()' && read"

# clean up
docker rm -f ssh-tunnel-client > /dev/null
}

create_database
generate_tunnel_keys
setup_remote_server
test_local_client

Running the above script should result in:

Running with Tunnel Public IP: n.n.n.n
Talking to remote db via tunnel to get server version: 8.0.28