The foundation is laid out in Part 1 & Part 2. Your command center is hardened, stealthed, and fortified against the mindless rabble of the public internet. It’s a silent, black monolith on the grid, invisible to scanners and hostile to intruders. But a fortress is useless without the infrastructure to project power.
Now, we build the operations wing. This is where we install the advanced tooling that turns our secure shell into a functional, powerful hub. We’ll deploy a modular application platform (Docker), establish a permanently encrypted communications channel for remote access (WireGuard), and set up a control panel that’s only visible from our private network. We’re not just locking the doors; we’re building a secret entrance that only we can see.
The rig is secure. It’s time to make it useful.
Phase 1: Fabricating the Application Platform
Our command center needs a way to run multiple, isolated operations without them interfering with each other. We won’t install software directly onto the main system; that’s messy and insecure. Instead, we’ll build a containment grid—a platform that lets us deploy any tool we need in its own armored, virtualized container. This is Docker.
First, we need to assemble the prerequisite components and cryptographic keys to connect to Docker’s secure fabrication repository.
Bash
# Update local manifest and install core transport tools
sudo apt update
sudo apt install -y ca-certificates curl gnupg
# Fabricate the keyring directory and download Docker's GPG signature
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Etch the repository location into our system's source list
# NOTE: Verify your Ubuntu codename. We're using 'noble' as the example.
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
With the connection to the repository secured, we can now pull down and install the Docker engine and its associated plugins.
Bash
# Resync with the newly added repo and install the Docker suite
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The containerization platform is now installed. To avoid needing sudo
for every single Docker command, which is like asking for a supervisor’s permission to open a drawer, we’ll add our operational alias to the docker
group.
Bash
# Replace 'USERNAME' with your actual handle
sudo usermod -aG docker USERNAME
Log out and log back in for this permission change to take effect. Your alias now has direct control over the container engine.
Phase 2: Establishing the Encrypted Tunnel (WireGuard)
Right now, our primary access is through a publicly exposed SSH port. It’s hardened, yes, but it’s still a visible attack surface. We’re going to change that. We will construct a permanent, point-to-point encrypted VPN tunnel directly into the command center. Once this is active, we can seal the public SSH port entirely, making our server disappear from the public internet while remaining fully accessible to us.
We’ll use WireGuard, a lean and powerful VPN protocol that acts like a wormhole between your client machine and the server.
First, install the WireGuard toolkit.
Bash
sudo apt update
sudo apt install -y wireguard
Next, we generate the cryptographic keys for the server-side of the tunnel. A private key that the server guards with its life, and a public key it can share with trusted peers (our clients).
Bash
# Generate and lock down the server's private key
sudo wg genkey | sudo tee /etc/wireguard/private.key
sudo chmod go= /etc/wireguard/private.key
# Derive the public key from the private key
sudo cat /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
Now, we create the tunnel’s configuration file. This defines the “shape” of our private network. Open the file for editing:
Bash
sudo nano /etc/wireguard/wg0.conf
Paste in the following configuration blueprint. This establishes a private network space (10.8.0.0/24
) and defines the server as the gateway at 10.8.0.1
.
Ini, TOML
[Interface]
# The server's private address inside the tunnel
Address = 10.8.0.1/24
# The UDP port the tunnel will listen on
ListenPort = 51820
# Your server's private key, which you must paste here
PrivateKey = PASTE_SERVER_PRIVATE_KEY_HERE
# Automatically save peer configurations when we add them
SaveConfig = true
# This is a placeholder for your first client device
[Peer]
PublicKey = PASTE_CLIENT_PUBLIC_KEY_HERE
AllowedIPs = 10.8.0.2/32
Replace PASTE_SERVER_PRIVATE_KEY_HERE
with the contents of your /etc/wireguard/private.key
file. We’ll fill in the client details in the next step.
Now, we must punch a hole in our firewall for the WireGuard tunnel’s traffic. Since we control both ends, this is a calculated and secure exception.
Bash
# Allow inbound traffic on our chosen UDP port
sudo ufw allow 51820/udp
sudo ufw reload
Finally, activate the tunnel and configure the system to automatically establish it on every boot.
Bash
# Bring the wg0 interface online
sudo systemctl start wg-quick@wg0
# Ensure it starts automatically
sudo systemctl enable wg-quick@wg0
To verify the tunnel is active, run sudo wg
. You should see the wg0
interface listening, though with no active peers yet.
Phase 3: Connecting Your Field Terminal
The server-side tunnel is active. Now we configure your client machine (your laptop or desktop) to connect to it.
- Install WireGuard on your local machine.
- Generate a key pair on your client. The commands are the same (
wg genkey | tee client.private | wg pubkey > client.public
), or you can use your WireGuard client’s GUI. - Create your client config file. This tells your machine how to find and connect to the server’s tunnel. Ini, TOML
[Interface]
# The private key you just generated for the client
PrivateKey = PASTE_CLIENT_PRIVATE_KEY_HERE
# The IP address your client will have inside the tunnel
Address = 10.8.0.2/24
# A good public DNS resolver for when the tunnel is active
DNS = 1.1.1.1
[Peer]
# The server's public key (from /etc/wireguard/public.key on the server)
PublicKey = PASTE_SERVER_PUBLIC_KEY_HERE
# Your server's public IP address and the port we opened
Endpoint = YOUR_SERVER_PUBLIC_IP:51820
# Defines what traffic should be sent through the tunnel.
# 10.8.0.0/24 sends traffic for our private network.
# 0.0.0.0/0 would send ALL traffic through the server.
AllowedIPs = 10.8.0.0/24
- Authorize the client on the server. Now, take the public key from your client and add it to the server’s configuration. Bash
# Replace CLIENT_PUBLIC_KEY with the actual key from your client sudo wg set wg0 peer CLIENT_PUBLIC_KEY allowed-ips 10.8.0.2/32
TheSaveConfig = true
option in ourwg0.conf
file automatically appends this new peer to the configuration, making it permanent.
Now, activate the tunnel from your client. You should be able to ping the server’s private IP (ping 10.8.0.1
) and get a response. You have established a secure, off-grid communications link.
Phase 4: The Off-Grid Command Console (Portainer)
With our encrypted tunnel active, we can deploy internal services that are completely invisible to the outside world. We’ll start with Portainer, a web-based GUI for managing our Docker containers, and Watchtower, an autonomous agent that will keep our deployed applications updated.
We’ll create a dedicated directory on the server to hold our container configurations.
Bash
# Create a directory structure for our docker operations
sudo mkdir -p /srv/docker/portainer
sudo nano /srv/docker/portainer/docker-compose.yml
Paste the following mission directive into the docker-compose.yml
file. This blueprint tells Docker exactly how to deploy and configure our new tools.
YAML
version: "3.8"
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
ports:
# CRITICAL: Bind port 9000 ONLY to the WireGuard tunnel's IP address.
# This makes it physically impossible to access from the public internet.
- "10.8.0.1:9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
# Automatically clean up old container images after an update
- WATCHTOWER_CLEANUP=true
# Check for new application versions every 5 minutes (300 seconds)
- WATCHTOWER_POLL_INTERVAL=300
volumes:
portainer_data:
Now, deploy the stack.
Bash
cd /srv/docker/portainer/
sudo docker compose up -d
Docker will pull the images and launch the containers in the background. While connected to your WireGuard VPN, open a browser and navigate to http://10.8.0.1:9000
. You’ll be greeted by the Portainer setup screen. Create your admin user. You are now looking at the command console for your entire application suite, accessible only from within your encrypted tunnel.
Final Step: Going Dark & Wiping Traces
Our work is done. The command center is fully operational with a secure, off-grid management layer. As a final act of operational security, we erase the command history from this setup session.
Bash
# Clear the history currently in memory
history -c
# Write the now-empty history to the log file, overwriting it
history -w
The slate is clean.
You now possess a hardened server with a containerized application platform, managed via a web interface that only you can see, through an encrypted tunnel that only you can access.
The kingdom is built. The walls are high. The gates are secret. Your work is done. For now. Lets move on to part 4 Fortifying the Gates with Nginx & Fail2ban.
Leave a Reply