Skip to content

Lab 13 โ€” Capstone: Harden a Containerized Application

Course: SCIA-120 ยท Introduction to Secure Computing
Topic: Capstone โ€” Applying Security Across All Domains
Difficulty: โญโญโญ Intermediate
Estimated Time: 90โ€“120 minutes
Related Reading: All chapters; emphasis on Ch. 4, 6, 7, 8, 11, 12, 13, 14


Overview

This capstone lab brings together everything you have learned in SCIA-120. You will start with a deliberately insecure containerized web application and systematically harden it by applying controls from across the course โ€” OS permissions, encryption, network isolation, authentication, and log monitoring. By the end, you will have transformed an insecure deployment into a hardened one and documented every change.


Learning Objectives

  1. Identify multiple security vulnerabilities in an insecure Docker deployment.
  2. Apply OS-level hardening (non-root user, read-only filesystem where possible).
  3. Enable HTTPS and disable plain HTTP.
  4. Isolate the application network from unnecessary exposure.
  5. Enable and review security-relevant logs.
  6. Document a security hardening checklist.

Prerequisites

  • All Labs 01โ€“12 should be completed or reviewed before starting this lab.
  • Docker Desktop installed and running.

Part 1 โ€” The Insecure Baseline

You are given a "production" deployment of a Python web application. Your task is to find and fix everything wrong with it.

Step 1.1 โ€” Create the Insecure Application

mkdir -p ~/lab13/app

Create the insecure Flask app:

cat > ~/lab13/app/app.py << 'EOF'
from flask import Flask, request, jsonify
import sqlite3, os

app = Flask(__name__)

# INSECURITY 1: Hardcoded credentials in code
DB_PASSWORD = "admin123"
SECRET_KEY = "mysecretkey"

def get_db():
    conn = sqlite3.connect('/tmp/users.db')
    conn.execute('''CREATE TABLE IF NOT EXISTS users
                    (id INTEGER PRIMARY KEY, username TEXT, password TEXT)''')
    conn.execute("INSERT OR IGNORE INTO users VALUES (1, 'alice', 'password123')")
    conn.execute("INSERT OR IGNORE INTO users VALUES (2, 'bob', 'letmein')")
    conn.commit()
    return conn

@app.route('/')
def index():
    return '<h1>Insecure App v1.0</h1><p>Welcome!</p>'

@app.route('/login')
def login():
    username = request.args.get('username', '')
    password = request.args.get('password', '')

    # INSECURITY 2: SQL injection vulnerability
    conn = get_db()
    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    result = conn.execute(query).fetchone()

    if result:
        return jsonify({"status": "logged in", "user": result[1]})
    return jsonify({"status": "failed"})

@app.route('/debug')
def debug():
    # INSECURITY 3: Debug endpoint exposes system info
    return jsonify({
        "env": dict(os.environ),
        "cwd": os.getcwd(),
        "files": os.listdir('/')
    })

if __name__ == '__main__':
    # INSECURITY 4: Debug mode on, listening on all interfaces
    app.run(host='0.0.0.0', port=5000, debug=True)
EOF

Create the insecure Dockerfile:

cat > ~/lab13/app/Dockerfile << 'EOF'
FROM python:3.11
# INSECURITY 5: Running as root (no USER directive)
RUN pip install flask
COPY app.py /app/app.py
WORKDIR /app
# INSECURITY 6: Debug mode, no TLS
CMD ["python", "app.py"]
EOF

Step 1.2 โ€” Build and Run the Insecure App

docker build -t insecure-app ~/lab13/app/
docker run -d \
  --name insecure-app \
  -p 5000:5000 \
  insecure-app

Step 1.3 โ€” Demonstrate the Vulnerabilities

Test SQL injection:

curl "http://localhost:5000/login?username=alice'--&password=wrong"

Test the debug endpoint (information disclosure):

curl http://localhost:5000/debug | python3 -m json.tool | head -20

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot of the SQL injection bypass working and the debug endpoint exposing sensitive information.

Step 1.4 โ€” Document the Vulnerabilities

Before hardening, list every vulnerability you can find. Create a Security Finding Report:

Finding Severity Description
Running as root High Container runs as root โ€” compromise = full system access
SQL Injection Critical Login endpoint vulnerable to SQLi authentication bypass
Debug endpoint High /debug exposes environment variables and filesystem listing
No HTTPS High Credentials sent over plaintext HTTP
Debug mode on Medium Flask debug mode enables interactive debugger โ€” RCE risk
Hardcoded credentials High Database password hardcoded in source code

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot of your completed vulnerability table.


Part 2 โ€” Stop the Insecure App

docker stop insecure-app && docker rm insecure-app

Part 3 โ€” Apply Hardening

Step 3.1 โ€” Fix the Application Code

cat > ~/lab13/app/app_secure.py << 'EOF'
from flask import Flask, request, jsonify
import sqlite3, os, hashlib

app = Flask(__name__)

# FIX 1: No hardcoded credentials โ€” use environment variables
SECRET_KEY = os.environ.get('SECRET_KEY', 'change-this-in-production')

def get_db():
    conn = sqlite3.connect('/tmp/users.db')
    conn.execute('''CREATE TABLE IF NOT EXISTS users
                    (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)''')
    # FIX 2: Store hashed passwords
    conn.execute("INSERT OR IGNORE INTO users VALUES (1, 'alice', ?)",
                 (hashlib.sha256(b'password123').hexdigest(),))
    conn.execute("INSERT OR IGNORE INTO users VALUES (2, 'bob', ?)",
                 (hashlib.sha256(b'letmein').hexdigest(),))
    conn.commit()
    return conn

@app.route('/')
def index():
    return '<h1>Secure App v2.0</h1><p>Welcome!</p>'

@app.route('/login')
def login():
    username = request.args.get('username', '')
    password = request.args.get('password', '')

    # FIX 3: Parameterized query โ€” no SQL injection
    conn = get_db()
    result = conn.execute(
        "SELECT * FROM users WHERE username=? AND password_hash=?",
        (username, hashlib.sha256(password.encode()).hexdigest())
    ).fetchone()

    if result:
        return jsonify({"status": "logged in", "user": result[1]})
    return jsonify({"status": "failed"})

# FIX 4: Debug endpoint REMOVED

if __name__ == '__main__':
    # FIX 5: debug=False, only listen on 127.0.0.1 (behind a proxy)
    app.run(host='127.0.0.1', port=5000, debug=False)
EOF

Step 3.2 โ€” Create the Hardened Dockerfile

cat > ~/lab13/app/Dockerfile.secure << 'EOF'
FROM python:3.11-slim

# FIX 6: Create non-root user
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

RUN pip install flask --no-cache-dir

COPY app_secure.py /app/app.py
WORKDIR /app

# FIX 7: Change ownership and switch to non-root user
RUN chown -R appuser:appgroup /app
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1

CMD ["python", "app.py"]
EOF

Step 3.3 โ€” Build the Hardened Image

docker build -t secure-app -f ~/lab13/app/Dockerfile.secure ~/lab13/app/

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot of the successful Docker build.


Part 4 โ€” Deploy with Network Isolation

Step 4.1 โ€” Create an Internal-Only Network for the App

docker network create --internal app-internal
docker network create app-external

Step 4.2 โ€” Generate TLS Certificate for HTTPS

mkdir -p ~/lab13/ssl
docker run --rm -v ~/lab13/ssl:/ssl ubuntu:22.04 bash -c "
apt-get install -y -qq openssl 2>/dev/null
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /ssl/server.key -out /ssl/server.crt \
  -subj '/C=US/ST=Maryland/L=Frostburg/O=FSU/CN=localhost'
chmod 600 /ssl/server.key
"

Step 4.3 โ€” Create Nginx Reverse Proxy Config (HTTPS Termination)

mkdir -p ~/lab13/nginx
cat > ~/lab13/nginx/nginx.conf << 'EOF'
events {}
http {
    server {
        listen 80;
        return 301 https://$host$request_uri;
    }
    server {
        listen 443 ssl;
        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-Content-Type-Options "nosniff";
        add_header X-XSS-Protection "1; mode=block";
        add_header Strict-Transport-Security "max-age=31536000";

        location / {
            proxy_pass http://secure-app:5000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}
EOF

Step 4.4 โ€” Deploy the Hardened Stack

# App container โ€” only on internal network
docker run -d \
  --name secure-app \
  --network app-internal \
  --read-only \
  --tmpfs /tmp \
  --cap-drop ALL \
  -e SECRET_KEY=prod-$(openssl rand -hex 16) \
  secure-app

# Nginx โ€” bridges internal and external
docker run -d \
  --name secure-nginx \
  --network app-external \
  -p 8080:80 \
  -p 8443:443 \
  -v ~/lab13/ssl:/etc/nginx/ssl:ro \
  -v ~/lab13/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx:alpine

docker network connect app-internal secure-nginx

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot of both containers running with docker ps.


Part 5 โ€” Verify the Hardening

Step 5.1 โ€” Verify Non-Root User

docker exec secure-app whoami

Expected: appuser โ€” not root.

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot showing whoami returns appuser.

Step 5.2 โ€” Verify SQL Injection is Fixed

curl -k "https://localhost:8443/login?username=alice'--&password=wrong"

Expected: {"status": "failed"} โ€” injection doesn't work anymore.

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot showing SQLi no longer bypasses auth.

Step 5.3 โ€” Verify Debug Endpoint is Gone

curl -k https://localhost:8443/debug

Expected: 404 Not Found.

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot showing the debug endpoint is removed.

Step 5.4 โ€” Verify HTTPS Redirect Works

curl -v http://localhost:8080/ 2>&1 | grep -E "Location|HTTP/"

Expected: 301 redirect to HTTPS.

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot of the HTTPโ†’HTTPS redirect.

Step 5.5 โ€” Verify App Is Not Directly Reachable (Only Through Nginx)

docker run --rm --network app-external curlimages/curl \
  curl -s http://secure-app:5000/ 2>&1

Expected: Connection refused โ€” the app container is only on the internal network.

๐Ÿ“ธ Screenshot checkpoint: Take a screenshot showing the app is unreachable directly.


Part 6 โ€” Final Hardening Checklist

Complete and submit this checklist with your screenshots:

Security Control Status Evidence (Screenshot #)
Application runs as non-root โœ… / โŒ
SQL injection patched โœ… / โŒ
Debug endpoint removed โœ… / โŒ
HTTPS enabled โœ… / โŒ
HTTP redirects to HTTPS โœ… / โŒ
Credentials not hardcoded โœ… / โŒ
Container uses --read-only filesystem โœ… / โŒ
Capabilities dropped (--cap-drop ALL) โœ… / โŒ
App network-isolated behind proxy โœ… / โŒ
Security headers present (HSTS, X-Frame, etc.) โœ… / โŒ

Cleanup

docker stop secure-app secure-nginx insecure-app 2>/dev/null
docker rm secure-app secure-nginx insecure-app 2>/dev/null
docker network rm app-internal app-external 2>/dev/null
docker rmi insecure-app secure-app 2>/dev/null
rm -rf ~/lab13
docker system prune -f

Lab Assessment

Screenshot Submission Checklist

  • [ ] screenshot-13a โ€” SQL injection bypassing insecure app
  • [ ] screenshot-13b โ€” Debug endpoint exposing env variables
  • [ ] screenshot-13c โ€” Vulnerability table (all 6 findings)
  • [ ] screenshot-13d โ€” Successful Docker build of hardened image
  • [ ] screenshot-13e โ€” Both containers running (docker ps)
  • [ ] screenshot-13f โ€” whoami showing non-root user
  • [ ] screenshot-13g โ€” SQL injection fixed (returns "failed")
  • [ ] screenshot-13h โ€” Debug endpoint returns 404
  • [ ] screenshot-13i โ€” HTTPโ†’HTTPS redirect
  • [ ] screenshot-13j โ€” App unreachable directly (only via proxy)
  • [ ] screenshot-13k โ€” Completed hardening checklist (all 10 items)

Final Reflection Essay

Write a one-page reflection (approximately 400โ€“500 words) addressing the following:

  1. What were the three most critical vulnerabilities in the original insecure application, and why did you rank them as most critical?
  2. Describe the defense-in-depth strategy you applied in this lab. How do the multiple layers (application code, OS user, network isolation, HTTPS) work together? What happens if one layer fails?
  3. Reflect on your learning across all 13 labs. Which concept surprised you most? Which do you think is most important for a real organization to get right?
  4. If you were advising a small company on improving their application security, what would be your top three recommendations based on what you learned in this course?

Grading Rubric

  • Screenshots complete with hardening checklist: 30 points
  • Vulnerability table (Part 1) fully completed: 20 points
  • Hardening correctly applied and verified: 20 points
  • Final reflection essay: 30 points
  • Total: 100 points

Congratulations!

You have completed all 13 SCIA-120 labs. You have hands-on experience with Docker security, file permissions, cryptography, network scanning, packet analysis, firewalls, TLS, SQL injection, malware sandboxing, SSH authentication, log analysis, and full application hardening. These skills form the practical foundation of a career in information security.