Securing Untrusted Code Execution: Building an Isolated CI/CD Pipeline with Drone CI and gVisor on a VPS
Introduction: The Hidden Danger in Standard CI/CD Pipelines
In modern software engineering, Continuous Integration and Continuous Deployment (CI/CD) pipelines are the backbone of rapid delivery. However, when workflows involve executing untrusted source code—such as processing multi-tenant workloads, evaluating student assignments, or running external open-source contributions—standard containerized pipelines introduce severe security liabilities. Shared kernel architectures mean that a single container escape can compromise the entire host virtual private server (VPS).
Traditional Docker-based CI/CD systems like Drone CI offer speed and flexibility by sharing the host Operating System (OS) kernel. If a malicious script triggers a kernel vulnerability, it can gain root access to the host machine. To mitigate this risk without incurring the massive financial and performance overhead of heavy virtual machines (VMs), engineering teams are turning to sandboxed container runtimes. This comprehensive guide walks you through architecting an isolated, multi-tenant-safe CI/CD infrastructure using Drone CI combined with Google's gVisor on a cost-effective VPS.
Understanding the Architecture: Drone CI and gVisor
Before diving into the technical implementation, it is crucial to understand why the combination of Drone CI and gVisor provides a superior security posture for high-risk code execution environment.
The Vulnerability of Standard Containers (runc)
Standard Docker containers utilize runc as their default runtime. While runc effectively segregates namespaces, cgroups, and filesystems, the processes inside the container still execute system calls (syscalls) directly against the host Linux kernel. If an application executes an exploit targeting a flaw in the host kernel, the boundary between the container and the host dissolves instantly.
How gVisor Redefines Container Security
Developed by Google, gVisor is an open-source container runtime that implements a user-space kernel. It intercepts all system calls made by the application inside the container. Instead of passing these syscalls directly to the host kernel, gVisor acts as a secure proxy. It consists of two primary components:
- Sentry: A user-space kernel written in Go that implements the vast majority of the Linux kernel API. It handles system calls internally, ensuring the application never talks directly to the host.
- Gofer: A file system proxy that isolates container file operations, preventing malicious file system traversal attacks.
By forcing Drone CI build steps to run within the gVisor runtime (known as runsc), any malicious code executed during the testing phase is entirely trapped inside a user-space sandbox, leaving the underlying VPS completely untouched.
Prerequisites and Environment Setup
To follow this tutorial, ensure your environment meets the following specifications:
- A clean VPS running Ubuntu 22.04 LTS or later with at least 2 vCPUs and 4GB of RAM.
- A public IP address with a domain name configured pointing to the VPS (required for Drone CI webhooks and SSL setup).
- A GitHub, GitLab, or Gitea account to act as the Version Control System (VCS) provider.
- Root or
sudoadministrative access to the server.
Step 1: Installing and Configuring Docker Engine
Drone CI operates natively as Docker containers. First, we must install the official Docker Engine on our Ubuntu VPS. Execute the following commands to update repositories and install dependencies:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL [https://download.docker.com/linux/ubuntu/gpg](https://download.docker.com/linux/ubuntu/gpg) | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] [https://download.docker.com/linux/ubuntu](https://download.docker.com/linux/ubuntu) $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-pluginVerify that Docker is active and running via systemd:
sudo systemctl status dockerStep 2: Installing Google gVisor (runsc)
With Docker established, the next phase is downloading and integrating the gVisor components. We will fetch the latest stable binaries for runsc and its corresponding containerd shim.
(
set -e
ARCH=$(uname -m)
URL="[https://storage.googleapis.com/gvisor/releases/release/latest/$](https://storage.googleapis.com/gvisor/releases/release/latest/$){ARCH}"
curl -LO "${URL}/runsc"
curl -LO "${URL}/runsc.sha256"
sha256sum -c runsc.sha256
chmod a+rx runsc
sudo mv runsc /usr/local/bin/
)Now, register gVisor as a legitimate runtime within the Docker daemon configuration. Edit or create the file located at /etc/docker/daemon.json:
{
"runtimes": {
"runsc": {
"path": "/usr/local/bin/runsc"
}
}
}Restart the Docker service to apply the configuration modifications seamlessly:
sudo systemctl restart dockerTo validate that gVisor is correctly integrated with Docker, execute a temporary container utilizing the runsc runtime and check the modified kernel node name:
docker run --rm --runtime=runsc alpine uname -aIf successfully isolated, the output will display a generic, simulated kernel identity string (e.g., Linux ... gVisor) instead of your VPS's actual host kernel details.
Step 3: Deploying Drone CI Server and Runner
Drone CI uses a distributed architecture consisting of a central Drone Server (manages state, webhooks, users, and dashboards) and one or more Drone Runners (polls the server for builds and executes them). We will deploy both via a unified docker-compose.yml file.
1. Create an OAuth Application
Navigate to your VCS provider (e.g., GitHub Settings -> Developer Settings -> OAuth Apps) and create a new application. Set the homepage to [https://drone.yourdomain.com](https://drone.yourdomain.com) and the Authorization Callback URL to [https://drone.yourdomain.com/login](https://drone.yourdomain.com/login). Note down the Client ID and Client Secret.
2. Write the Compose Configuration
Create a dedicated directory and write the deployment manifest:
mkdir ~/drone-ci && cd ~/drone-ci
nano docker-compose.ymlPopulate the file with the following configuration layout, substituting your actual configuration parameters:
version: '3.8'
services:
drone-server:
image: drone/drone:2
container_name: drone-server
ports:
- "8080:80"
volumes:
- ./drone-data:/data
environment:
- DRONE_GITHUB_CLIENT_ID=your_github_client_id
- DRONE_GITHUB_CLIENT_SECRET=your_github_client_secret
- DRONE_RPC_SECRET=generate_a_secure_random_string_here
- DRONE_SERVER_HOST=drone.yourdomain.com
- DRONE_SERVER_PROTO=https
restart: always
drone-runner:
image: drone/drone-runner-docker:1
container_name: drone-runner
depends_on:
- drone-server
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_RPC_PROTO=http
- DRONE_RPC_HOST=drone-server
- DRONE_RPC_SECRET=generate_a_secure_random_string_here
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME=isolated-vps-runner
restart: alwaysLaunch the system stack using Docker Compose:
docker compose up -dNote: For production deployments, wrap the Drone Server port 8080 behind a reverse proxy like Nginx or Caddy configured with Let's Encrypt certificates to fulfill the https protocol requirement specified above.
Step 4: Enforcing gVisor Execution globally for Drone Builds
By default, the Drone Docker Runner communicates with /var/run/docker.sock and invokes standard containers utilizing the default host runtime (runc). To guarantee total isolation for untrusted code, we must instruct the runner to strictly enforce runsc for every single build block pipeline it spins up.
Update the environment section of your drone-runner service within the docker-compose.yml file to incorporate the DRONE_RUNNER_ENVIRON configuration:
environment:
- DRONE_RPC_PROTO=http
- DRONE_RPC_HOST=drone-server
- DRONE_RPC_SECRET=generate_a_secure_random_string_here
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME=isolated-vps-runner
- DRONE_DOCKER_RUNTIME=runscThe critical addition is DRONE_DOCKER_RUNTIME=runsc. This forces the runner daemon to pass the gVisor execution flag to the Docker daemon API whenever an integration agent lifecycle begins.
Recreate the runner container to apply the modifications:
docker compose up -d --force-recreate drone-runnerStep 5: Verifying the Isolated Pipeline in Production
To verify that our defensive design functions seamlessly under real-world scenarios, let's create a test repository including a pipeline configuration specifically written to probe the boundaries of the executing system environment.
Create a .drone.yml file inside your target code repository:
kind: pipeline
type: docker
name: untrusted-code-evaluation
steps:
- name: analyze-runtime-environment
image: alpine:latest
commands:
- echo "Evaluating system integrity inside build environment..."
- uname -a
- dmesg || echo "Kernel logs successfully protected!"
- cat /proc/cpuinfo | grep -i "model name" || trueCommit and push this configuration file to your repository. When Drone intercepts the push webhook, it will trigger the build pipeline. Reviewing the execution logs will reveal the structural security characteristics implemented:
- Isolated Kernel context: The output of
uname -awill match the sandboxed gVisor footprint. - Blocked Host access: The
dmesgcommand will fail entirely or return an empty space, proving the isolated process cannot read host kernel ring buffers. - Abbreviated CPU Profiles: The virtualized hardware information presented through
/proc/cpuinfowill hide the physical attributes of your underlying VPS processors.
Conclusion: Secure Automation Without High Infrastructure Cost
By layering Google’s gVisor sandbox underneath an automated Drone CI engine, you successfully isolate the execution context of external commits and untrusted scripts on a single, affordable VPS. System calls are cleanly intercepted, and any attempt at executing malicious escalations or utilizing container escape methodologies is immediately neutralized at the user-space layer.
This implementation guarantees that your central development workloads can scale safely alongside third-party expansions or automated code-evaluation modules, maintaining strict security compliances without complex multi-server orchestrations or expensive cloud allocations.
