Hosting Machine Challenges
TODO: Docker macvlan networking
Machine challenges can spawn a new environment of machines for players, which can be done using QEMU. Players will need to connect to a VPN (e.g., WireGuard) to access the dynamically created machines.
The VPN will grant users FULL access to your Docker network or Kubernetes pods network.For Kubernetes, you should write a NetworkPolicy to restrict users' access to machine challenges. For Docker, ensure that Raya and your databases are running on a separate Docker network.
VPN Setup (Docker)
I personally use WireGuard with wg-easy.
The server is configured to route traffic to the Docker network.
Update
10.254.0.0/16
to match your Docker network.
WG_ALLOWED_IPS=10.8.0.0/24, 10.254.0.0/16
POST_UP
iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE; iptables -A FORWARD -i wg0 -d 10.254.0.0/16 -j ACCEPT; iptables -A FORWARD -i wg0 -s 10.254.0.0/16 -j ACCEPT
POST_DOWN
iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE; iptables -D FORWARD -i wg0 -d 10.254.0.0/16 -j ACCEPT; iptables -D FORWARD -i wg0 -s 10.254.0.0/16 -j ACCEPT
VPN setup (Kubernetes)
The setup is not different from the docker setup above, just change the docker network address to the Pods CIDR.
Be careful not to disrupt your CNI. For instance, if you're using Calico, it might be configured to use the first detected network interface to determine the node IP. It's possible that wg0 could be the first detected. To avoid this, add a regular expression to skip wg.* interfaces
Machine image
Currently, I deploy machine challenges as a docker container that runs QEMU, the image has a copy of the machine, each time a container is started virt-customize
updates the machine image (patch flag.txt
so that it has a dynamic flag / add a dynamic user / add a machine environment variable / etc..)
FROM debian:12-slim AS builder
RUN apt-get update && \
apt-get install -y qemu-system-x86 libguestfs-tools pwgen && \
rm -rf /var/lib/apt/lists/*
# The OVA file of the challenge
COPY ctf-image.ova /root/ctf-image.ova
RUN tar -xvf /root/ctf-image.ova -C /root/
RUN rm /root/ctf-image.ova
RUN qemu-img convert -f vmdk -O qcow2 /root/*.vmdk /root/ctf-image.qcow2
RUN rm /root/*.vmdk
FROM debian:12-slim AS runner
RUN apt-get update && \
apt-get install -y qemu-system-x86 libguestfs-tools pwgen && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /root/ctf-image.qcow2 /root/ctf-image.qcow2
# Environment variables
ENV VM_MEMORY=1G
ENV VM_CPU=1
# Start script to set up and run the VM
COPY start_vm.sh /root/start_vm.sh
RUN chmod +x /root/start_vm.sh
# Run the start script
CMD ["/root/start_vm.sh"]
#!/bin/bash
VM_IMAGE="/root/ctf-image.qcow2"
echo "Generated SSH password for ctf-player: $SSH_PASSWORD" > /root/ssh_password.txt
echo "Generated SSH password for ctf-player: $SSH_PASSWORD"
# Add a player user
virt-customize --format qcow2 -a "$VM_IMAGE" --password "ctf-player:password:$SSH_PASSWORD"
# Add flag.txt
echo "$FLAG" > flag.txt
# Add a dynamic flag.txt to the machine
virt-customize --format qcow2 -a "$VM_IMAGE" --upload flag.txt:/home/ctf-player/flag.txt
# change the Redis password
virt-customize --format qcow2 -a "$VM_IMAGE" --run-command "sed -i 's/^requirepass .*/requirepass $FLAG/' /etc/redis/redis.conf"
# Ports used by the machine
PORTS_TO_FORWARD=("22" "8000" "8080" "443")
# build hostfwd arguments
HOSTFWD_ARGS=""
for PORT in "${PORTS_TO_FORWARD[@]}"; do
HOSTFWD_ARGS="$HOSTFWD_ARGS,hostfwd=tcp::${PORT}-:${PORT}"
done
# trim the leading comma
HOSTFWD_ARGS="${HOSTFWD_ARGS#,}"
qemu-system-x86_64 -hda "$VM_IMAGE" -m "$VM_MEMORY" \
-enable-kvm -smp "$VM_CPU" -boot c -nographic -serial mon:stdio \
-device e1000,netdev=net0 \
-netdev user,id=net0,$HOSTFWD_ARGS
PreInstanceCreation.lua
local function generate_hex(length)
local hex = ""
for i = 1, length do
hex = hex .. string.format("%x", math.random(0, 15))
end
return hex
end
local password = generate_hex(10)
raya.docker_options = {
devices = {"/dev/net/tun", "/dev/kvm"},
environment = {string.format("SSH_PASSWORD=%s", password)}
}
PostInstanceCreation.lua
raya.description = string.format([[
Connect to the VPN.
#### Connection details
```sh
ssh ctf-player@%s
```
#### Password
```
%s
```
]], raya.container.container_ip_address, string.match(raya.docker_options.environment[1], "SSH_PASSWORD=(%w+)"))
raya.instance = {
show_port = false,
host = raya.container.container_ip_address
}