Skip to content

Latest commit

 

History

History
174 lines (136 loc) · 7.67 KB

File metadata and controls

174 lines (136 loc) · 7.67 KB

BrokenOps Lab Format Guide

This guide explains how to create new intentionally broken labs for the BrokenOps platform.

Each lab must be placed in its own folder under the labs/ directory (e.g., labs/nginx-broken/).

1. lab.yaml (Required)

This is the core definition of your lab. It defines metadata, VM specs, and verification info.

id: "nginx-broken"
name: "Nginx Service Down"
category: "linux"
difficulty: "beginner"
description:
  summary: "Nginx is not running after reboot"
vm:
  name: "nginx-lab"
  memory: 1024 # Memory in MB
  cpu: 1
  disk: "10G" # Disk overlay size
cloud_init: "cloud-init.yaml"
verify_script: "verify.sh"
exposed_ports:
  - 80
  • verify_script: Name of the verification script inside the lab folder.
  • exposed_ports: (Optional) List of ports that the web UI should provide "Open" buttons for. Adding a port does not make the lab easier by itself; it only tells BrokenOps that users should be able to reach that service through the browser proxy.
  • port_works_initially: (Optional, default false) If true, the CI test will ensure that the exposed ports successfully return an HTTP 200 response upon initial lab provision. Use this only when the intended broken state is somewhere else and the exposed service is supposed to be healthy from the start.
  • When a port is exposed, the service must listen on an interface reachable from outside the VM, not just 127.0.0.1. The browser proxy connects to the VM over its IP address, so loopback-only listeners will still fail even if curl http://127.0.0.1:<port> works inside the guest.
  • If you want the port to remain exposed while the lab is still broken, keep exposed_ports enabled and make the initial state fail for a different reason, such as a bad upstream address, a missing runtime directory, or an ACL/firewall problem. The goal is for the browser Open Port button to work both before and after the user fixes the lab, without any surprise “works in the guest, fails in the UI” behavior.

Exposed Port Guidance

If a lab exposes a port, the service inside the VM must be reachable from outside the guest, not just from 127.0.0.1 inside the VM.

Important rules:

  • Bind the service to 0.0.0.0 or the VM interface that the platform can reach.
  • Do not rely on localhost-only listeners for a port that should work from the Open Port UI.
  • curl http://127.0.0.1:<port> working inside the VM is not enough if the UI proxy connects by the VM IP.
  • If the lab is meant to teach a localhost-only or socket-bound service, omit exposed_ports instead of exposing a port that cannot be reached.
  • If port_works_initially: false, the initial broken state should still be intentional and reproducible; the exposed port should become reachable only after the user fixes the lab.

Example listener setup that works with the UI proxy:

python3 -m http.server 4000 --bind 0.0.0.0

Recommended checklist before opening a PR with exposed ports:

  1. Confirm the service is listening on 0.0.0.0:<port> or the correct non-loopback interface after the solution.
  2. Confirm the initial failure is the one the lab is teaching, not an accidental proxy or binding problem.
  3. Confirm the browser/Open Port path works after the solution, not just a local shell curl.
  4. Keep exposed_ports in lab.yaml when the UI should show the port; do not remove it just to make CI pass.

initial_access (Optional)

Determines the privilege level of the SSH session presented to the player in the web terminal.

  • full (default): The terminal connects as root with full privileges. This is the standard behavior for all existing labs.
  • restricted: The terminal connects as a non-privileged user (opsuser by default). The player must exploit a misconfiguration (e.g., a writable sensitive file, a setuid binary, or an overly permissive sudo rule) to escalate to root.

When initial_access: restricted is set, the backend automatically:

  1. Creates the configured restricted user in cloud-init with the same SSH key used for root.
  2. Connects the web terminal as that restricted user instead of root.
  3. Continues to run verify.sh and solution.sh as root internally (backend bypass), so CI pipelines are unaffected.

To customize the restricted username, add restricted_user alongside initial_access:

initial_access: restricted
restricted_user: intern

If omitted, the backend uses opsuser as the default restricted account name.

Example:

initial_access: restricted

Important notes for restricted labs:

  • The lab's cloud-init.yaml must intentionally break root access (e.g., change the root password to a random value, remove the restricted user from the sudo group).
  • The lab must provide a realistic escalation path (e.g., sudo misconfiguration, writable /etc/passwd, or a SUID binary).
  • verify.sh should assert that the player has successfully restored normal access (e.g., the restricted user is back in the sudo group and /etc/sudoers is valid).

2. cloud-init.yaml (Required)

This file tells the ubuntu-24.04-base.qcow2 image how to configure itself on first boot. Use this to intentionally break the system.

#cloud-config
packages:
  - nginx

runcmd:
  # Intentional break: configure nginx to listen on invalid port to break it
  - sed -i 's/listen 80 default_server;/listen 80808 default_server;/' /etc/nginx/sites-available/default
  - systemctl restart nginx || true

3. verify.sh (Required)

A bash script executed by the backend via SSH to score the user's progress.

  • Print custom output explaining what passed or failed.
  • Exit with status 0 if the lab is fully solved (Score 100).
  • Exit with status >0 if the lab is incomplete (Score 0).
#!/bin/bash
systemctl is-active --quiet nginx
if [ $? -eq 0 ]; then
  echo "SUCCESS: Nginx is running!"
  exit 0
else
  echo "FAILURE: Nginx is not running."
  exit 1
fi

4. solution.sh (Required)

A mandatory bash script that automatically fixes the broken lab. The CI verification pipeline will execute this script via SSH and then run verify.sh to ensure the lab is fully solved. If this script is missing or fails, the CI build will fail.

#!/bin/bash
# Fix the nginx configuration
sed -i 's/listen 80808 default_server;/listen 80 default_server;/' /etc/nginx/sites-available/default
systemctl restart nginx

5. question.md & solution.md (Required)

Markdown files containing the task description and the solution guide.

  • question.md: Presented immediately to the user.
  • solution.md: Hidden behind a "Reveal Solution" button that only appears after the user attempts Verification.

Both files MUST follow these strict format templates.

question.md Template

### Scenario
[A short real-world story that explains who noticed the issue and what broke]

### Objective
[What the learner must restore, diagnose, or verify]

### Useful Commands
- `command1`
- `command2`
- `command3`

solution.md Template

### The Issue
[A brief explanation of the root cause]

### Step-by-Step Fix
1. **[Inspect / confirm / diagnose]**:
   - Explain what to look at and why.
   - Include a command only when it helps the learner confirm the state.
2. **[Make the fix manually]**:
   - Prefer a step-by-step edit or service action.
   - If you include a command, keep it as an optional helper rather than a full automation script.
3. **[Verify]**:
   - Show the expected outcome after the fix.

Writing style notes

  • Keep question.md scenarios concrete and realistic.
  • Use consistent headings across labs.
  • Avoid turning solution.md into a shell script; it should read like a guided fix, with optional commands such as sed when they help explain the manual step.