Setting up OpenVPN for CTF
Once, a task was set to create a custom stand for holding CTF competitions. The tasks were prepared in the format of Docker containers, but they required a network.
The logical question arises: “Why?” The network will allow you to allocate a separate IP address to each task. In principle, of course, you can do without a network, but this approach has several disadvantages, namely:
Services have non-standard ports.
All tasks are deployed to one address, so the important task of scanning ports is removed from the task. Participants will immediately know them, and it will only remain to specify the service.
Manually distributing ports. In my tasks, there was mostly more than one service per task. So there is a need for logical separation of tasks. And there may also be problems with automatic task launch.
The tasks involve using a reverse shell, and if the service is deployed on the Internet, users will not be able to use it without a white address. It turns out that 99% of users will not be able to solve the task without using, for example, a VPS
I would like to restrict access to the Internet for tasks so that after a successful attack the user cannot use the machine's resources for his own purposes.
Briefly about what I wanted to get:
Tasks should be in an isolated network, without Internet access, but if necessary, it should be possible to issue it.
Each participant must have an individual IP address.
Each task must have an individual IP address.
There should be no restrictions on the ports used by each task.
After solving the problem, the user should not use the resources for his own purposes.
One VPN config for all competition participants.
The article covers the possibility of working with both Docker containers and virtual machines. However, if any point is not needed, it can be skipped.
Software and hardware
Proxmox 8.1.4 was used as a hypervisor, and a Debian 12 virtual machine was used as a router.
To understand the structure of the project, the network diagram is presented below:
The question may arise: “Why OpenVPN?”
Wireguard could be considered as an alternative, however:
Running its configs is a bit more complicated for the user.
OpenVPN also allows you to connect multiple clients using one config.
OpenVPN is already a standard in CTF competitions.
Basic setup. System update
Debian 12 was taken as a basis. We update the system and install software for basic work:
sudo apt update && sudo apt upgrade -y && sudo apt install -y curl vim nano wget net-tools iptables-persistent
IP Forward
Next, you need to enable IP forward in the system:
sudo nano /etc/sysctl.conf
Edit the parameter: net.ipv4.ip_forward = 1.
You need to set the value to 1
Static address on the network
Since our device will act as a server and router, getting an address via DHCP will not be the best solution. Therefore, it is necessary to specify a static address:
Edit the file /etc/network/interfaces in any convenient text editor:
sudo nano /etc/network/interfaces
Let's add the following lines to configure a static IP address:
auto ens18
iface ens18 inet static address 192.168.0.155
netmask 255.255.255.0
gateway 192.168.0.1
dns-nameservers 8.8.8.8
After rebooting the server will receive the address 192.168.0.155
sudo systemctl restart NetworkManager.service
Installing OpenVPN
This project was taken as a basis: https://github.com/kylemanna/docker-openvpn.
But we will deploy it immediately on the main system for ease of working with the rules:
Let's change the user to root:
sudo su -
Install the necessary packages:
sudo apt install -y openvpn iptables bash git easy-rsa pamtester
Let's write a link to use files from /usr/share/easy-rsa/easyrsa
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin
Let's register environment variables for the scripts to work:
echo "export OPENVPN=/etc/openvpn" >> ~/.bashrc
echo "export EASYRSA=/usr/share/easy-rsa"
>> ~/.bashrcecho "export EASYRSA_CRL_DAYS=3650" >> ~/.bashrc echo "export EASYRSA_PKI=$OPENVPN/pki" >> ~/.bashrc
Activate the config
~/.bashrc
to make the environment variables active:source ~/.bashrc
We clone the repository and go to the directory:
git clone https://github.com/kylemanna/docker-openvpn cd docker-openvpn
We copy the necessary files and grant rights:
cp ./bin/* /usr/local/bin
cp ./otp/openvpn /etc/pam.d/ chmod a+x /usr/local/bin/* cd
Next, we perform the configuration. Instead of (HERE_YOU_NEED_SPECIFY_THE_SERVER_IP_ADDRESS), we specify the server's IP address:
ovpn_genconfig -N -d -u udp://ТУТ_НУЖНО_УКАЗАТЬ_IP_АДРЕС_СЕРВЕРА
Example key:
Qwertyqwertyqwerty123
Next, follow the text instructions:
Generate configs for participants. When creating, you will be asked for the key we entered above:
easyrsa build-client-full ctf-team nopass
Save the config to a file:
ovpn_getclient ctf-team > ./ctf-team.ovpn
Since we need participants to be able to connect to the network through one config, let's edit the config:
vim /etc/openvpn/openvpn.conf
Add the line:
duplicate-cn
Launch OpenVPN:
ovpn_run
Download the server config from the path
/root/ctf-team.ovpn
to your computer.It might look like this:
scp root@SERVER_IP:/root/ctf-team.ovpn ~
And we try to launch it.
Launches on the main system:
sudo openvpn ctf-team.ovpn
If everything works, then we try to restart the server and check again. OpenVPN should start on its own.
Setting up Docker
Let's install Docker: sudo apt install -y docker.io docker-compose
Network configuration
We go to the console again so that the rights are updated, and create a docker network: docker network create --subnet=192.168.158.0/24 ctf-net
You can read more about working with Docker network here: https://habr.com/ru/companies/otus/articles/730798/
Open the VPN config that we downloaded from the server and add the following lines to it:
route 192.168.255.0 255.255.255.0
route 192.168.158.0 255.255.255.0
If these two lines are present, then everything is done correctly:
192.168.158.0/24 via 192.168.255.5 dev tun0
192.168.255.0/24 via 192.168.255.5 dev tun0
iptables rules
We write rules for iptables:
sudo iptables -A FORWARD -i tun0 -s 192.168.255.0/24 -d 192.168.158.0/24 -j ACCEPT
sudo iptables -A FORWARD -o tun0 -s 192.168.158.0/24 -d 192.168.255.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Разрешим трафик от машин (192.168.158.0/24) к сети (192.168.255.0/24) sudo iptables -A FORWARD -o tun0 -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
sudo iptables -A FORWARD -i tun0 -s 192.168.255.0/24 -d 192.168.158.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
Network tests
Network tests before container
On the server, we'll set up a test container with nginx:
docker run --network=ctf-net -d --rm --name nginx_test nginx
After which we get the address of this container in the docker network:
docker inspect nginx_test | grep IPAddress
In this case it is 192.168.158.2 so on the main system with the VPN connected in the browser I go to the site 192.168.158.2
Network test from container
Let's check in the opposite direction.
I get my address through the ifconfig command:
docker exec -ti nginx_test /bin/bash
After which I get the console inside the container from the root user.
By default, there is no ping in the basic nginx container, so it needs to be installed:
apt update && apt install iputils-ping
After that we check the clients' access to the VPN:
Everything works, so we exit the container with the command exit
Save iptables rules
Now you need to save the iptables rules.
Launching CTF tasks
Everything is simple here: any container launched with the parameter: --network=ctf-net
will immediately be configured and will work with the network we need.
So, running tasks will look like this: docker run --network=ctf-net -d --rm --name CONTAINER_NAME IMAGES_NAME
Setting up access to an isolated network of virtual machines
Docker is great, but tasks can also be deployed to virtual machines, so you need to enable them too.
To do this, we will create a virtual network in our hypervisor.
Then we connect our machine to this network with a new network interface.
Here we can see that ens19 is left without an address, since there is no DHCP server in the isolated network. Therefore, it is necessary to register a static address on the interface and install a DHCP server.
Installing DHCP (This module can be collapsed)
sudo apt install isc-dhcp-server
Editing the config:
sudo nano /etc/dhcp/dhcpd.conf
We add this to the empty space:
subnet 10.0.50.0 netmask 255.255.255.0 {
range 10.0.50.10 10.0.50.240;
option domain-name-servers 8.8.8.8;
option subnet-mask 255.255.255.0;
option routers 10.0.50.1;
default-lease-time 600;
max-lease-time 7200;
}
***I would like to draw attention to the range 10.0.50.10 10.0.50.240 .
Here the range of IP addresses distributed by the DHCP server is set. That is, 2-10 and 240-254 can be used as service addresses. For example, monitoring services or services with a mandatory static address.
Edit the following file:
sudo nano /etc/default/isc-dhcp-server
You need to specify the interface to which the DHCP server will distribute addresses.
Now it's time to test it out.
iptables rules
Add the route to the OpenVPN config that you downloaded from the server.
We write rules for iptables. (Run as root)
Please note the name of the interfaces. They will probably be called differently.
iptables -A FORWARD -i tun0 -o ens19 -s 192.168.255.0/24 -d 10.0.50.0/24 -j ACCEPT
iptables -A FORWARD -i ens19 -o tun0 -s 10.0.50.0/24 -d 192.168.255.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Разрешим трафик от машин (10.0.50.0/24) к сети (192.168.255.0/24) iptables -A FORWARD -i ens19 -o tun0 -s 10.0.50.0/24 -d 192.168.255.0/24 -j ACCEPT
iptables -A FORWARD -i tun0 -o ens19 -s 192.168.255.0/24 -d 10.0.50.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
After that, we try to ping the client connected to the VPN from the virtual machine:
We keep the new rules:
iptables-save > /etc/iptables/rules.v4
At this point, this stage of configuration for virtual machines can be considered complete.
Additional iptables rules
Pay attention to the interface names. They will probably be called differently.
It is advisable to make a backup of the virtual machine so as not to accidentally break anything.
Let's allow connections via ssh:
sudo iptables -A INPUT -i ens18 -s 192.168.0.1/24 -p tcp --dport 22 -j ACCEPT
Let's open access to the VPN udp port:
sudo iptables -A INPUT -i ens18 -p udp --dport 1194 -j ACCEPT
Let's apply the default policy:
sudo iptables -P FORWARD DROP
Internet access ban for tun:
iptables -I FORWARD -i tun0 -o ens18 -j DROP
iptables -I FORWARD -i ens18 -o tun0 -j DROP
Internet Ban for Docker
The container has the ability to access the Internet by default, but we are not happy with this, so we need to add a rule prohibiting this:
iptables -I FORWARD -s 192.168.158.0/24 -j DROP
iptables -I FORWARD -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
Internet access ban for VM
Since we do not have rules allowing traffic to move to the Internet, the virtual machine will not be able to access the Internet by default.
Internet permission for Docker
To allow internet access, you need to remove the rule.
This can be done like this:
iptables -D FORWARD -s 192.168.158.0/24 -j DROP
Revert the rule back and block access to the Internet:
iptables -D FORWARD -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
iptables -I FORWARD -s 192.168.158.0/24 -j DROP
iptables -I FORWARD -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
Internet access permission for VM
This is a bit more complicated. You need to activate packet forwarding, which can be done using MASQUERADE. And also allow packet forwarding for addresses 10.0.50.0/24
iptables -t nat -A POSTROUTING -o ens18 -s 10.0.50.0/24 -j MASQUERADE
iptables -A FORWARD -i ens19 -o ens18 -s 10.0.50.0/24 -j ACCEPT
iptables -A FORWARD -i ens18 -o ens19 -d 10.0.50.0/24 -j ACCEPT
Please note that ens18 is the name of the interface that looks at the Internet.
Also the reverse task, namely, to return the ban on traffic:
iptables -t nat -D POSTROUTING -o ens18 -s 10.0.50.0/24 -j MASQUERADE
iptables -D FORWARD -i ens19 -o ens18 -s 10.0.50.0/24 -j ACCEPT
iptables -D FORWARD -i ens18 -o ens19 -d 10.0.50.0/24 -j ACCEPT
Saving the rules
After the changes we needed, we saved the rules: iptables-save > /etc/iptables/rules.v4
Since when you reboot, rules that were not saved will be automatically deleted, this is very convenient to use.
On this stand, containers and virtual machines do not have Internet access by default, but if I need to download something for configuration, I simply write a rule for this, and after work I turn off the stand.
As a result, when the team comes to attack the stand, there is no Internet on the tasks according to the standard, and there is no need to remember whether there was permission to enter or not.
Conclusion and some thoughts
It won't be difficult to assemble a stand for holding CTF competitions or experiments.
Initially, the project was made to hold competitions
at the university, but in the end it turned out
really convenient, and several copies were deployed for different tasks.
(One is even at home, for local experiments with friends. Since you can connect to such a network remotely and have your own stand for training.)
As mentioned earlier, there were competitions at the institute, so I really wanted to monitor the load in real time. The competition was held entirely on Docker containers, and the domolo project is perfect for tracking their resources: https://github.com/ductnn/domolo.git
Just download and run via docker-compose. As a result, we have statistics on resources for each container. So, if someone runs a conditional miner in a container, it will be immediately visible, and this container can be quickly destroyed.