Securely connecting Civo and AWS with StrongSwan VPN
Learn how to set up a secure site-to-site VPN between Civo and AWS using StrongSwan, enabling private connectivity and secure communication between your cloud resources.
Written by
Founder @ KubeNine
Written by
Founder @ KubeNine
Connecting workloads across clouds has become a common requirement for teams that rely on hybrid environments. Organizations migrating to Civo Cloud, or expanding into it, often need a stable way to reach private AWS resources without exposing anything publicly. But this is where challenges appear quickly.
Most teams discover that cross-cloud connectivity is often expensive, complicated, and filled with vendor-specific requirements. AWS Direct Connect, proprietary firewalls, or managed VPNs introduce cost or administrative weight that small teams don’t always want to deal with.
Fortunately, there's a practical answer: a StrongSwan-powered site-to-site VPN running on a Civo VM that connects directly to an AWS VPC. This setup creates a secure tunnel where both cloud networks can communicate as if they belonged to the same private environment.
This tutorial provides a complete, end-to-end walkthrough—from planning IP address space and configuring AWS networking components, to deploying StrongSwan on Civo, establishing redundant IPsec tunnels, and extending connectivity to additional virtual machines.
Why cross-cloud private connectivity matters
Many organizations reach a point where hosting everything in one cloud is no longer viable. New products might be easier to run in a different provider, or teams may want to migrate workloads gradually while keeping backend systems in AWS.
In most of these scenarios, engineers face a similar challenge:
“How do we allow Civo workloads to reach AWS private resources without placing those resources on the public internet?”
A public endpoint is often unacceptable for:
- Internal APIs
- Databases
- Legacy systems
- Sensitive or regulated workloads
A site-to-site VPN addresses this problem by securely linking two private networks, so they behave as a single routed environment.
AWS supports IPsec-based VPNs via a Virtual Private Gateway (VGW). Civo, meanwhile, provides full control over virtual machines and networking, making StrongSwan a flexible and cost-effective VPN solution.
Prerequisites
To get started with this tutorial, you will need the following in place:
- A Civo virtual machine
- StrongSwan
- Non-overlapping CIDR ranges
- Standard AWS VPN components
The result is a secure IPsec tunnel that routes private traffic between Civo and AWS.
Planning the network layout
A stable VPN configuration begins with clean network planning. The most important rule is simple:
Your AWS VPC CIDR and Civo network CIDR must not overlap.
The following reference layout is used throughout this tutorial:
These CIDRs must remain consistent across AWS configuration, StrongSwan setup, and static routing.
Preparing the AWS side
This entire setup depends on defining the AWS components correctly. The following AWS pieces are required:
- A VPC
- Subnets
- A Virtual Private Gateway (VGW)
- A Customer Gateway (pointing to StrongSwan’s public IP)
- A Site-to-Site VPN connection
- A static route directing Civo traffic through the VPN
Create the AWS VPC
In the AWS console:
- Navigate to VPC → Create VPC
- Name:
mumbai-main-vpc - IPv4 CIDR:
10.10.0.0/16 - Tenancy: Default
Create subnets
You’ll need at least two subnets (the database subnet should not require internet access):
(Optional) Internet gateway
An internet gateway is only required if some AWS resources need outbound internet access. It is not required for the VPN itself. If you need more information on this, refer to this documentation.
Launch AWS private resources
Example configuration:
- Application instance
- Subnet:
10.10.1.0/24 - IP:
10.10.1.10 - Security group: allow traffic only from
192.168.50.0/24
- Subnet:
- Private database
- Subnet:
10.10.2.0/24 - IP:
10.10.2.106 - Security group: allow database port (for example, 3306 or 5432) from
192.168.50.0/24
- Subnet:
This ensures private access exclusively through the VPN tunnel.
Creating AWS VPN components
AWS must now be configured to recognise the Civo VPN endpoint.
Create a Customer Gateway (CGW)
- Console → VPC → Customer Gateways → Create
- Name:
civo-strongswan-cgw - IP address: Public IP of the Civo VM
- Routing: Static
- BGP ASN: Any valid value
Create a Virtual Private Gateway (VGW)
- Console → VPC → Virtual Private Gateways → Create
- Name:
mumbai-vgw - Attach it to the VPC
Create the site-to-site VPN
- Console → VPC → Site-to-Site VPN Connections → Create
- Name:
aws-civo-vpn - Target:
mumbai-vgw - Customer Gateway:
civo-strongswan-cgw - Routing: Static
- Static Route:
192.168.50.0/24(Civo CIDR)
Wait for it to enter the Available state.
Download the VPN configuration file
Choose:
- Vendor: strongSwan
- Platform: Ubuntu
- IKE version: IKEv2
This file contains tunnel endpoints, pre-shared keys, encryption parameters, and tunnel interface IPs used later in the configuration.
Deploying StrongSwan on Civo
A single Civo virtual machine acts as the VPN gateway. StrongSwan supports route-based VPNs using VTI interfaces, which align well with AWS VGW requirements.
The provided script is a production-ready deployment tool that:
- Installs StrongSwan
- Enables IP forwarding
- Configures two redundant tunnels
- Creates VTI interfaces
- Applies static routes and firewall rules
- Starts and validates both tunnels
Rather than duplicating the entire script inline, this guide focuses on how it works and how to adapt it safely.
Key variables to update
Inside the script, update:
PUBLIC_IPLOCAL_SUBNETAWS_CIDRAWS_T1_PUBLIC,AWS_T2_PUBLICAWS_T1_PSK,AWS_T2_PSKT1_LOCAL_IP,T1_REMOTE_IP,T2_LOCAL_IP,T2_REMOTE_IP
These values all come directly from the AWS VPN configuration file.
Setup-vpn.sh
#!/bin/bash############################################### AWS VPN TUNNEL SETUP SCRIPT (Route-Based)## This script sets up a route-based IPsec VPN tunnel between# a Civo VM and AWS VPC using strongSwan.## TO USE ON A NEW VM:# 1. Update the variables below with your values# 2. Run: sudo bash script.sh# 3. The script is idempotent - safe to run multiple times############################################################################################## CHANGE ONLY THESE VALUES FOR NEW VM##############################################PUBLIC_IP="212.2.249.102" # Your Civo VM PUBLIC IP (must match AWS Customer Gateway) `leftid`LOCAL_SUBNET="192.168.50.0/24" # Your Civo network CIDR `leftsubnet`AWS_CIDR="10.10.0.0/16" # AWS VPC CIDR `rightsubnet`# AWS VPN Tunnel Endpoints (from AWS VPN config file)AWS_T1_PUBLIC="13.235.15.233" # AWS Tunnel 1 Public IPAWS_T2_PUBLIC="65.0.248.54" # AWS Tunnel 2 Public IP# Pre-Shared Keys (from AWS VPN config file)AWS_T1_PSK="xxxxx"AWS_T2_PSK="xxxxx"# Route-based VPN tunnel IPs (from AWS VPN config file)# These are the 169.254.x.x addresses for the tunnel interfacesT1_LOCAL_IP="169.254.26.210/30" # Tunnel1 local IPT1_REMOTE_IP="169.254.26.209/30" # Tunnel1 remote IPT2_LOCAL_IP="169.254.19.106/30" # Tunnel2 local IPT2_REMOTE_IP="169.254.19.105/30" # Tunnel2 remote IP# Get physical interface name (usually eth0 or ens5)PHYS_INTERFACE=$(ip route | grep default | awk '{print $5}' | head -1)if [ -z "$PHYS_INTERFACE" ]; thenPHYS_INTERFACE="eth0" # fallbackfi############################################### INSTALL STRONGSWAN##############################################echo " Installing strongSwan..."sudo apt update && sudo apt install -y strongswan netfilter-persistent############################################### ENABLE IP FORWARDING##############################################echo "Enabling IP forwarding..."echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-ipforward.confecho "net.ipv4.conf.all.accept_redirects=0" | sudo tee -a /etc/sysctl.d/99-ipforward.confecho "net.ipv4.conf.all.send_redirects=0" | sudo tee -a /etc/sysctl.d/99-ipforward.confsudo sysctl -p############################################### CREATE /etc/ipsec.conf (ROUTE-BASED VPN)##############################################echo " Writing /etc/ipsec.conf..."sudo tee /etc/ipsec.conf > /dev/null << EOFconfig setupcharondebug="all"uniqueids=yesstrictcrlpolicy=noconn Tunnel1type=tunnelauto=addkeyexchange=ikev2authby=pskleft=%anyleftid=$PUBLIC_IPleftsubnet=$LOCAL_SUBNETright=$AWS_T1_PUBLICrightsubnet=$AWS_CIDRaggressive=noikelifetime=28800slifetime=3600smargintime=270srekey=yesrekeyfuzz=100%fragmentation=yesreplay_window=1024dpddelay=30sdpdtimeout=120sdpdaction=restartike=aes128-sha1-modp1024esp=aes128-sha1-modp1024keyingtries=%forevermark=100leftupdown="/etc/ipsec.d/aws-updown.sh -ln Tunnel1 -ll $T1_LOCAL_IP -lr ${T1_REMOTE_IP%/*}/30 -m 100 -r $AWS_CIDR"conn Tunnel2type=tunnelauto=addkeyexchange=ikev2authby=pskleft=%anyleftid=$PUBLIC_IPleftsubnet=$LOCAL_SUBNETright=$AWS_T2_PUBLICrightsubnet=$AWS_CIDRaggressive=noikelifetime=28800slifetime=3600smargintime=270srekey=yesrekeyfuzz=100%fragmentation=yesreplay_window=1024dpddelay=30sdpdtimeout=120sdpdaction=restartike=aes128-sha1-modp1024esp=aes128-sha1-modp1024keyingtries=%forevermark=200leftupdown="/etc/ipsec.d/aws-updown.sh -ln Tunnel2 -ll $T2_LOCAL_IP -lr ${T2_REMOTE_IP%/*}/30 -m 200 -r $AWS_CIDR"EOF############################################### CREATE PSK FILE##############################################echo " Writing /etc/ipsec.secrets..."sudo tee /etc/ipsec.secrets > /dev/null << EOF$PUBLIC_IP $AWS_T1_PUBLIC : PSK "$AWS_T1_PSK"$PUBLIC_IP $AWS_T2_PUBLIC : PSK "$AWS_T2_PSK"EOF############################################### 4.CREATE AWS UPDOWN SCRIPT##############################################echo " Creating AWS updown script..."sudo mkdir -p /etc/ipsec.dsudo tee /etc/ipsec.d/aws-updown.sh > /dev/null << 'UPSCRIPT'#!/bin/bashwhile [[ $# > 1 ]]; docase ${1} in-ln|--link-name)TUNNEL_NAME="${2}"TUNNEL_PHY_INTERFACE="${PLUTO_INTERFACE}"shift;;-ll|--link-local)TUNNEL_LOCAL_ADDRESS="${2}"TUNNEL_LOCAL_ENDPOINT="${PLUTO_ME}"shift;;-lr|--link-remote)TUNNEL_REMOTE_ADDRESS="${2}"TUNNEL_REMOTE_ENDPOINT="${PLUTO_PEER}"shift;;-m|--mark)TUNNEL_MARK="${2}"shift;;-r|--static-route)TUNNEL_STATIC_ROUTE="${2}"shift;;*)echo "${0}: Unknown argument \"${1}\"" >&2;;esacshiftdonecommand_exists() {type "$1" >&2 2>&2}create_interface() {ip link add ${TUNNEL_NAME} type vti local ${TUNNEL_LOCAL_ENDPOINT} remote ${TUNNEL_REMOTE_ENDPOINT} key ${TUNNEL_MARK} 2>/dev/null || trueip addr add ${TUNNEL_LOCAL_ADDRESS} dev ${TUNNEL_NAME} 2>/dev/null || trueip link set ${TUNNEL_NAME} up mtu 1419}configure_sysctl() {sysctl -w net.ipv4.ip_forward=1sysctl -w net.ipv4.conf.${TUNNEL_NAME}.rp_filter=2sysctl -w net.ipv4.conf.${TUNNEL_NAME}.disable_policy=1sysctl -w net.ipv4.conf.${TUNNEL_PHY_INTERFACE}.disable_xfrm=1sysctl -w net.ipv4.conf.${TUNNEL_PHY_INTERFACE}.disable_policy=1}add_route() {# Get local IP from the physical interfacePHYS_IF=$(ip route | grep default | awk '{print $5}' | head -1)LOCAL_IP=$(ip addr show ${PHYS_IF} 2>/dev/null | grep "inet " | awk '{print $2}' | cut -d/ -f1)if [ -z "$LOCAL_IP" ]; thenLOCAL_IP="${TUNNEL_LOCAL_ENDPOINT}"fiIFS=',' read -ra route <<< "${TUNNEL_STATIC_ROUTE}"for i in "${route[@]}"; do# Remove existing route first, then add with source IPip route del ${i} dev ${TUNNEL_NAME} 2>/dev/null || trueip route add ${i} dev ${TUNNEL_NAME} src ${LOCAL_IP} metric ${TUNNEL_MARK} 2>/dev/null || truedoneiptables -t mangle -C FORWARD -o ${TUNNEL_NAME} -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || \iptables -t mangle -A FORWARD -o ${TUNNEL_NAME} -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuiptables -t mangle -C INPUT -p esp -s ${TUNNEL_REMOTE_ENDPOINT} -d ${TUNNEL_LOCAL_ENDPOINT} -j MARK --set-xmark ${TUNNEL_MARK} 2>/dev/null || \iptables -t mangle -A INPUT -p esp -s ${TUNNEL_REMOTE_ENDPOINT} -d ${TUNNEL_LOCAL_ENDPOINT} -j MARK --set-xmark ${TUNNEL_MARK}# Mark outgoing traffic based on routeiptables -t mangle -C OUTPUT -o ${TUNNEL_NAME} -j MARK --set-xmark ${TUNNEL_MARK} 2>/dev/null || \iptables -t mangle -A OUTPUT -o ${TUNNEL_NAME} -j MARK --set-xmark ${TUNNEL_MARK}ip route flush table 220 2>/dev/null || true}cleanup() {IFS=',' read -ra route <<< "${TUNNEL_STATIC_ROUTE}"for i in "${route[@]}"; doip route del ${i} dev ${TUNNEL_NAME} metric ${TUNNEL_MARK} 2>/dev/null || truedoneiptables -t mangle -D FORWARD -o ${TUNNEL_NAME} -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || trueiptables -t mangle -D INPUT -p esp -s ${TUNNEL_REMOTE_ENDPOINT} -d ${TUNNEL_LOCAL_ENDPOINT} -j MARK --set-xmark ${TUNNEL_MARK} 2>/dev/null || trueiptables -t mangle -D OUTPUT -o ${TUNNEL_NAME} -j MARK --set-xmark ${TUNNEL_MARK} 2>/dev/null || trueip route flush cache 2>/dev/null || true}delete_interface() {ip link set ${TUNNEL_NAME} down 2>/dev/null || trueip link del ${TUNNEL_NAME} 2>/dev/null || true}case "${PLUTO_VERB}" inup-client)create_interfaceconfigure_sysctladd_route;;down-client)cleanupdelete_interface;;esacUPSCRIPTsudo chmod 744 /etc/ipsec.d/aws-updown.sh############################################### 4. CONFIGURE CHARON (DISABLE AUTO ROUTES)##############################################echo " Configuring charon.conf..."if [ -f /etc/strongswan.d/charon.conf ]; thensudo sed -i 's/# install_routes = yes/install_routes = no/' /etc/strongswan.d/charon.confif ! grep -q "install_routes = no" /etc/strongswan.d/charon.conf; thenecho "install_routes = no" | sudo tee -a /etc/strongswan.d/charon.conffifi# Note: VTI interfaces will be created by the updown script when tunnels come up############################################### FLUSH OLD STATE & RESTART VPN##############################################echo "♻ Restarting VPN..."# Clean up any existing routes to AWS CIDRsudo ip route del $AWS_CIDR via 192.168.50.5 2>/dev/null || truesudo ip route del $AWS_CIDR dev enp1s0 2>/dev/null || truesudo ip route del $AWS_CIDR dev $PHYS_INTERFACE 2>/dev/null || truesudo ip route del $AWS_CIDR dev Tunnel1 2>/dev/null || truesudo ip route del $AWS_CIDR dev Tunnel2 2>/dev/null || truesudo ip xfrm state flushsudo ip xfrm policy flushsudo ipsec restartsleep 5# Start the tunnelsecho " Starting tunnels..."sudo ipsec up Tunnel1sudo ipsec up Tunnel2sleep 3# Routes are added by the updown script, but verify they have correct source IPecho " Verifying routes..."LOCAL_IP=$(ip addr show $PHYS_INTERFACE | grep "inet " | awk '{print $2}' | cut -d/ -f1)if [ -n "$LOCAL_IP" ]; then# Ensure routes have correct source IP (updown script should have done this, but double-check)sudo ip route del $AWS_CIDR dev Tunnel1 2>/dev/null || truesudo ip route del $AWS_CIDR dev Tunnel2 2>/dev/null || truesudo ip route add $AWS_CIDR dev Tunnel1 src $LOCAL_IP metric 100 2>/dev/null || truesudo ip route add $AWS_CIDR dev Tunnel2 src $LOCAL_IP metric 200 2>/dev/null || truefiecho " Tunnel status:"sudo ipsec statusall############################################### FORWARD TRAFFIC (REQUIRED FOR VM2,3,4...)##############################################echo "🔧 Allowing forwarded traffic..."sudo iptables -C FORWARD -s $LOCAL_SUBNET -d $AWS_CIDR -j ACCEPT 2>/dev/null || \sudo iptables -A FORWARD -s $LOCAL_SUBNET -d $AWS_CIDR -j ACCEPTsudo iptables -C FORWARD -s $AWS_CIDR -d $LOCAL_SUBNET -j ACCEPT 2>/dev/null || \sudo iptables -A FORWARD -s $AWS_CIDR -d $LOCAL_SUBNET -j ACCEPTsudo netfilter-persistent save############################################### TESTING INSTRUCTIONS FOR YOU##############################################echo ""echo " TESTING COMMANDS:"echo "-------------------------------------------"echo "sudo ipsec statusall # Check tunnel status"echo "ip route show # Check routes"echo "ip addr show Tunnel1 # Check Tunnel1 interface"echo "ip addr show Tunnel2 # Check Tunnel2 interface"echo "sudo tcpdump -n -i any proto esp # Monitor ESP traffic"echo "ping -c 4 10.10.2.106 # Test ping to AWS"echo "-------------------------------------------"echo "If you have another VM (192.168.50.10+):"echo "sudo ip route add 10.10.0.0/16 via <STRONGSWAN_LOCAL_IP>"echo "-------------------------------------------"echo " If tcpdump shows ESP packets — TUNNEL IS WORKING!"echo ""
Running the script
SSH into your Civo VM, then run:
sudo bash Setup-vpn.sh
The script will:
- Install packages
- Create
/etc/ipsec.conf - Create
/etc/ipsec.secrets - Create
/etc/ipsec.d/aws-updown.sh - Restart StrongSwan
- Bring up Tunnel1 and Tunnel2
- Add routes for AWS CIDR
You should see output indicating that both tunnels come up successfully.
Understanding what the script creates
Testing the VPN
Once the StrongSwan VM is configured, it’s time to verify connectivity.
Extending connectivity to other Civo VMs
Once the VPN gateway is operational, other VMs in the same Civo network can reach AWS resources by adding a single route:
sudo ip route add 10.10.0.0/16 via 192.168.50.5
Test it by running:
ping -c 4 10.10.1.10
If ping works, the routing path is correct.
Making static routes persistent
Routes added manually disappear after reboot. To make them persistent, use one of the options below.
Option A: Netplan (Ubuntu 18.04+)
Edit:
sudo nano /etc/netplan/50-cloud-init.yaml
Add under your network interface:
network:version: 2ethernets:enp1s0:dhcp4: trueroutes:- to: 10.10.0.0/16via: 192.168.50.5
Apply:
sudo netplan apply
Option B: Older Ubuntu versions
Edit:
sudo nano /etc/network/interfaces
Add:
up ip route add 10.10.0.0/16 via 192.168.50.5
Now AWS routing persists across reboots.
How traffic moves across the VPN
Here’s the flow for a VM in the Civo network:
Civo VM (192.168.50.10)↓Route: 10.10.0.0/16 via 192.168.50.5↓StrongSwan VM (VPN Gateway)↓VTI Tunnel↓AWS Virtual Private Gateway↓AWS VPC Resource (10.10.x.x)
This architecture makes both clouds feel linked through a private connection.
Summary
Cross-cloud networking is often associated with complexity, cost, and restrictive vendor tooling. With Civo, StrongSwan, and standard AWS VPN components, it is possible to build a secure, private, and production-ready cloud-to-cloud connection without unnecessary overhead.
This architecture enables:
- Private database access
- Secure service-to-service communication
- Gradual migration from AWS to Civo
- Hybrid and multi-cloud deployments without public exposure
If you're looking for a straightforward method to connect AWS and Civo in a way that keeps traffic private and under your control, this StrongSwan VPN architecture is a strong option. With proper planning and the script provided earlier, you can have both clouds communicating through a secure tunnel in minutes.

Founder @ KubeNine
Vikas Yadav is the founder of Kubenine and a DevOps engineer focused on building scalable cloud infrastructure and developer platforms. His work centers on automation, platform engineering, and modern DevOps practices using technologies such as AWS, Kubernetes, Terraform, and CI/CD pipelines.
With more than seven years of experience in cloud engineering, including previous work at LinkedIn, Vikas helps startups and enterprise teams improve deployment workflows, automate infrastructure, and optimize cloud environments.
Share this article
Further Reading
19 September 2024
Self hosting GitHub Actions runner on Civo
3 April 2024