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¶
- Identify multiple security vulnerabilities in an insecure Docker deployment.
- Apply OS-level hardening (non-root user, read-only filesystem where possible).
- Enable HTTPS and disable plain HTTP.
- Isolate the application network from unnecessary exposure.
- Enable and review security-relevant logs.
- 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¶
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:
Test the debug endpoint (information disclosure):
๐ธ 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¶
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¶
๐ธ 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¶
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¶
Expected: appuser โ not root.
๐ธ Screenshot checkpoint: Take a screenshot showing whoami returns appuser.
Step 5.2 โ Verify SQL Injection is Fixed¶
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¶
Expected: 404 Not Found.
๐ธ Screenshot checkpoint: Take a screenshot showing the debug endpoint is removed.
Step 5.4 โ Verify HTTPS Redirect Works¶
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)¶
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โwhoamishowing 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:
- What were the three most critical vulnerabilities in the original insecure application, and why did you rank them as most critical?
- 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?
- 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?
- 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.