This repository contains the build instructions for creating a single standalone platform that is design to host websites and resources for conducting cyber security tabletop exercises.
- You have a fresh Ubuntu Server 24.04 install with sudo access.
- Static IP
192.168.1.100(swap in your own throughout). - Hostname
ripple. - A Wi-Fi access point / switch connected to the box, with the AP handing out this server's IP as the DNS server.
Below are a set of instructions to build the platform from scratch. As time permits, this repo will be expanded to include additional features and code to run on the platform itself.
sudo apt install bind9 nginx apache2 mysql-server unzipsudo su -
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -yTest the install:
docker run hello-worldsudo vi /etc/apache2/ports.confChange Listen 80 to an unused port (this example uses 8181):
Listen 8181
<IfModule ssl_module>
Listen 443
</IfModule>
<IfModule mod_gnutls.c>
Listen 443
</IfModule>
sudo vi /etc/apache2/sites-enabled/000-default.confUpdate the first line to match:
<VirtualHost *:8181>
Restart apache
sudo systemctl restart apache2sudo mkdir /etc/bind/zones
sudo vi /etc/bind/zones/db.localZone contents (adjust hostname/IP to your own):
$TTL 3D
$ORIGIN local.
local. IN SOA ripple.local. hostmaster.local. (
2026070201 ; serial
8H ; refresh
4H ; retry
4W ; expire
1D ; minimum
)
NS ripple.local.
MX 10 mail.local.
localhost A 127.0.0.1
ripple A 192.168.1.100
mail A 192.168.1.100
cyberchef A 192.168.1.100
ctf A 192.168.1.100
fakebook A 192.168.1.100
cert A 192.168.1.100
why A 192.168.1.100
things A 192.168.1.100
Visit https://gchq.github.io/CyberChef/ to obtain the latest version
sudo wget https://gchq.github.io/CyberChef/CyberChef_v10.19.4.zip
sha256sum CyberChef_v10.19.4.zipMove files into the right folder for serving
cd /var/www/html/
sudo mkdir cyberchef.local
sudo cp ~/CyberChef_v10.19.4.zip ./cyberchef.local/
cd cyberchef.local
sudo unzip CyberChef_v10.19.4.zip
sudo rm CyberChef_v10.19.4.zip
sudo mv CyberChef_v10.19.4.html ./index.htmlNginx site config:
cd /etc/nginx/sites-available/
sudo vi cyberchef.localserver {
listen 80;
listen [::]:80;
root /var/www/html/cyberchef.local;
index index.html index.htm index.nginx-debian.html;
server_name cyberchef.local;
location / {
try_files $uri $uri/ =404;
}
}Enable the site:
cd ../sites-enabled/
sudo ln -s /etc/nginx/sites-available/cyberchef.local ./cyberchef.localTest and restart:
sudo nginx -t
sudo systemctl restart nginx bind9Visit http://cyberchef.local.
cd ~
sudo git clone https://github.com/CTFd/CTFd.git
cd CTFd/
sudo head -c 64 /dev/urandom > .ctfd_secret_keyEdit the compose file:
sudo vi docker-compose.ymlUnder the nginx service, change the port mapping from:
ports:
- 80:80to:
ports:
- "8000:80"Bring the stack up:
sudo docker compose up --detachReverse proxy for ctf.local:
sudo vi /etc/nginx/sites-available/ctf.localserver {
listen 80;
listen [::]:80;
server_name ctf.local;
location / {
proxy_pass http://127.0.0.1:8000;
}
}Enable and restart:
sudo ln -s /etc/nginx/sites-available/ctf.local /etc/nginx/sites-enabled/ctf.local
sudo nginx -t
sudo systemctl restart nginx bind9Visit http://ctf.local.
sudo apt install postfixChoose Internet Site or Local only at the mail-config prompt (Local only is fine if you never need this box to talk to the outside world) and set the hostname (example: intramail.local - add this to DNS as covered in section 4).
sudo vi /etc/postfix/main.cfChange:
inet_interfaces = all
Add:
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
smtpd_sasl_auth_enable = yes
smtpd_sasl_auth_type = dovecot
smtpd_sasl_path = private/auth
virtual_mailbox_domains = /etc/postfix/virtual_mailbox_domains
virtual_transport = lmtp:unix:private/dovecot/lmtp
virtual_mailbox_base = /var/mail/vhosts
Virtual domains:
sudo vi /etc/postfix/virtual_mailbox_domainsexample1.local #example1.local
example2.local #example2.local
sudo postmap /etc/postfix/virtual_mailbox_domainsRe-run postmap any time this file changes.
Enable submission:
sudo vi /etc/postfix/master.cfUncomment:
submission inet n - y - - smtpd
sudo apt install dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpdMail location:
sudo vi /etc/dovecot/conf.d/10-mail.confChange:
mail_location = mbox:~/mail:INBOX=/var/mail/%u
to:
mail_location = maildir:/var/mail/vhosts/%d/%n
Create vhost folders (repeat per domain) and the vmail user:
sudo mkdir -p /var/mail/vhosts/example.local
sudo groupadd -g 5000 vmail
sudo useradd -r -g vmail -u 5000 vmail -d /var/mail/vhosts -c "Virtual mail user"
sudo chown -R vmail:vmail /var/mail/vhosts/Enable IMAP/POP3:
sudo vi /etc/dovecot/conf.d/10-master.conf...
inet_listener imap {
port = 143
}
...
inet_listener pop3 {
port = 110
}
...
Enable LMTP - find:
...
service lmtp {
unix_listener lmtp {
#mode = 0666
}
#inet_listener lmtp {
#address =
#port =
#}
}
...
Change to:
...
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
...
Enable Postfix SMTP-AUTH - find:
...
#unix_listener /var/spool/postfix/private/auth {
# mode = 0666
#}
...
Change to:
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
Auth settings:
sudo vi /etc/dovecot/conf.d/10-auth.confdisable_plaintext_auth = no
auth_mechanisms = plain login
Comment out:
#!include auth-system.conf.ext
Uncomment:
!include auth-passwdfile.conf.ext
Passdb/userdb config:
sudo vi /etc/dovecot/conf.d/auth-passwdfile.conf.extpassdb {
driver = passwd-file
args = scheme=PLAIN username_format=%u /etc/dovecot/dovecot-users
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n
}
User/password file:
sudo vi /etc/dovecot/dovecot-usersadmin@example.local:{plain}password
info@example.local:{plain}password
admin@example2.local:{plain}password
Passwords are stored in plaintext here - acceptable only because this is an isolated, offline training box. Not recommended to use this in any sort of production environment.
sudo apt install -y php-mysql php-mbstring php-bcmath php-zip php-curl php-xml php-fpm
sudo apt install -y roundcubeDuring install, select Yes to configure a database, choose MySQL, and set/note a DB password.
DNS entry:
sudo vi /etc/bind/zones/db.localintramail.local A 192.168.1.100
Nginx site:
sudo vi /etc/nginx/sites-available/intramail.localserver {
listen 80;
listen [::]:80;
server_name intramail.local;
root /var/lib/roundcube;
index index.html index.htm index.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}Default host:
sudo vi /etc/roundcube/config.inc.php$config['default_host'] = 'localhost';Firewall + restart:
sudo ufw allow 25/tcp
sudo ufw allow 110/tcp
sudo ufw allow 143/tcp
sudo systemctl restart nginx apache2 bind9 dovecot postfixVisit http://intramail.local.
wget https://wordpress.org/latest.zip
sudo mkdir /var/www/html/example.localDNS entry:
sudo vi /etc/bind/zones/db.localexample.local A 192.168.1.100
Nginx site:
sudo vi /etc/nginx/sites-available/example.localserver {
listen 80;
listen [::]:80;
server_name example.local;
root /var/www/html/example.local;
index index.html index.htm index.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}Deploy WordPress files:
cp ~/latest.zip /var/www/html/example.local/
cd /var/www/html/example.local/
unzip latest.zip
mv ./wordpress/* ./
rm -r ./wordpress
rm latest.zipDatabase:
sudo mysqlCREATE USER 'example_user'@'localhost' IDENTIFIED BY 'password';
CREATE DATABASE example_local;
GRANT ALL PRIVILEGES ON example_local.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;wp-config:
cp /var/www/html/example.local/wp-config-sample.php /var/www/html/example.local/wp-config.php
vi /var/www/html/example.local/wp-config.phpdefine( 'DB_NAME', 'example_local' );
define( 'DB_USER', 'example_user' );
define( 'DB_PASSWORD', 'password' );Repeat this whole section for each team/site you need.
Finally, fix ownership across the web root:
sudo chown -R www-data:www-data /var/www/htmlAt the end of this, you should have a platform that is able to serve website content and a work email system that can be used between participants when running an exercise.