first commit
This commit is contained in:
commit
e2b9dc0c2f
37
.gitea/workflows/build-push.yaml
Normal file
37
.gitea/workflows/build-push.yaml
Normal file
@ -0,0 +1,37 @@
|
||||
name: Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Harbor
|
||||
run: |
|
||||
echo '${{ secrets.HARBOR_TOKEN }}' | docker login YOUR_REGISTRY -u '${{ secrets.HARBOR_USERNAME }}' --password-stdin
|
||||
|
||||
- name: Build and Push Docker image
|
||||
run: |
|
||||
IMAGE=YOUR_REGISTRY/${{ vars.HARBOR_PROJECT }}/phoenix:${{ gitea.sha }}
|
||||
docker build -t $IMAGE .
|
||||
docker push $IMAGE
|
||||
docker tag $IMAGE YOUR_REGISTRY/${{ vars.HARBOR_PROJECT }}/phoenix:latest
|
||||
docker push YOUR_REGISTRY/${{ vars.HARBOR_PROJECT }}/phoenix:latest
|
||||
echo "Pushed: $IMAGE"
|
||||
|
||||
- name: Update deployments repo
|
||||
run: |
|
||||
git clone https://gitea-actions:${{ secrets.DEPLOY_TOKEN }}@YOUR_GITEA/${{ gitea.repository_owner }}/deployments.git
|
||||
cd deployments
|
||||
sed -i "s|image: .*/phoenix:.*|image: YOUR_REGISTRY/${{ vars.HARBOR_PROJECT }}/phoenix:${{ gitea.sha }}|" phoenix/deployment.yaml
|
||||
git config user.name "gitea-actions"
|
||||
git config user.email "gitea-actions@localhost"
|
||||
git add phoenix/deployment.yaml
|
||||
git diff --cached --quiet || git commit -m "chore: update phoenix image to ${{ gitea.sha }}"
|
||||
git push || (git pull --rebase && git push)
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM python:3.13
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY app.py .
|
||||
|
||||
RUN pip install --no-cache-dir flask
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Phoenix App
|
||||
|
||||
**WARNING: This application contains intentional security vulnerabilities. DO NOT deploy on production clusters.**
|
||||
|
||||
A deliberately insecure application for Kubernetes hardening training.
|
||||
|
||||
## Purpose
|
||||
|
||||
The app demonstrates common Kubernetes misconfigurations and vulnerabilities, including:
|
||||
- Remote Code Execution (RCE)
|
||||
- Overpermissive RBAC
|
||||
- Privileged containers
|
||||
- Host filesystem access
|
||||
- Host PID/Network namespace access
|
||||
|
||||
Attendees learn to defend against these attacks through progressive infrastructure hardening.
|
||||
279
app.py
Normal file
279
app.py
Normal file
@ -0,0 +1,279 @@
|
||||
from flask import Flask, request, render_template_string
|
||||
import subprocess
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import ssl
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
DEBUG_PATH = os.environ.get('DEBUG_PATH')
|
||||
DEPLOYMENT_NAME = os.environ.get('DEPLOYMENT_NAME', 'phoenix-app')
|
||||
|
||||
DASHBOARD_HTML = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Phoenix</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
color: #111;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
font-size: 1.125rem;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status-line strong {
|
||||
color: #16a34a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
details {
|
||||
margin-top: 2rem;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
padding: 0.5rem 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
summary:hover {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.details-table {
|
||||
margin-top: 1rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.details-table td {
|
||||
padding: 0.375rem 0;
|
||||
}
|
||||
|
||||
.details-table td:first-child {
|
||||
color: #888;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.details-table td:last-child {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
color: #16a34a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Phoenix</h1>
|
||||
|
||||
{% if error %}
|
||||
<p class="status-line error-text"><strong>{{ error }}</strong></p>
|
||||
{% else %}
|
||||
<p class="status-line">Phoenix is up and running for <strong>{{ uptime }}</strong></p>
|
||||
|
||||
<details {% if details_open %}open{% endif %}>
|
||||
<summary>Details</summary>
|
||||
<table class="details-table">
|
||||
<tr><td>Deployment</td><td>{{ deployment }}</td></tr>
|
||||
<tr><td>Status</td><td class="status-running">{{ status }}</td></tr>
|
||||
<tr><td>Replicas</td><td>{{ replicas }}</td></tr>
|
||||
<tr><td>Namespace</td><td>{{ namespace }}</td></tr>
|
||||
<tr><td>Created</td><td>{{ created }}</td></tr>
|
||||
</table>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
def get_deployment_info():
|
||||
"""Fetch deployment info from K8s API using mounted ServiceAccount token."""
|
||||
try:
|
||||
with open('/var/run/secrets/kubernetes.io/serviceaccount/token') as f:
|
||||
token = f.read().strip()
|
||||
with open('/var/run/secrets/kubernetes.io/serviceaccount/namespace') as f:
|
||||
namespace = f.read().strip()
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.load_verify_locations('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt')
|
||||
|
||||
api_url = f'https://kubernetes.default.svc/apis/apps/v1/namespaces/{namespace}/deployments/{DEPLOYMENT_NAME}'
|
||||
req = urllib.request.Request(api_url, headers={'Authorization': f'Bearer {token}'})
|
||||
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=5) as resp:
|
||||
deploy = json.loads(resp.read())
|
||||
|
||||
created_str = deploy['metadata']['creationTimestamp']
|
||||
created_dt = datetime.fromisoformat(created_str.replace('Z', '+00:00'))
|
||||
now = datetime.now(timezone.utc)
|
||||
uptime_delta = now - created_dt
|
||||
|
||||
days = uptime_delta.days
|
||||
hours, remainder = divmod(uptime_delta.seconds, 3600)
|
||||
minutes, _ = divmod(remainder, 60)
|
||||
|
||||
if days > 0:
|
||||
uptime = f"{days}d {hours}h {minutes}m"
|
||||
elif hours > 0:
|
||||
uptime = f"{hours}h {minutes}m"
|
||||
else:
|
||||
uptime = f"{minutes}m"
|
||||
|
||||
ready = deploy.get('status', {}).get('readyReplicas', 0)
|
||||
desired = deploy.get('spec', {}).get('replicas', 1)
|
||||
status = 'Running' if ready == desired else 'Degraded'
|
||||
|
||||
return {
|
||||
'deployment': DEPLOYMENT_NAME,
|
||||
'namespace': namespace,
|
||||
'status': status,
|
||||
'replicas': f"{ready}/{desired}",
|
||||
'created': created_dt.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
||||
'uptime': uptime,
|
||||
'error': None
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {'error': 'Not running in Kubernetes'}
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 403:
|
||||
return {'error': 'Unable to fetch deployment details (forbidden)'}
|
||||
return {'error': f'Unable to fetch deployment details ({e.code})'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
HTML = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Debug Console</title>
|
||||
<style>
|
||||
body { font-family: monospace; background: #1a1a1a; color: #0f0; padding: 20px; }
|
||||
input { width: 80%; padding: 10px; font-size: 16px; background: #000; color: #0f0; border: 1px solid #0f0; }
|
||||
button { padding: 10px 20px; background: #0f0; color: #000; border: none; cursor: pointer; }
|
||||
pre { background: #000; padding: 15px; border: 1px solid #333; overflow-x: auto; white-space: pre-wrap; }
|
||||
h1 { color: #0f0; }
|
||||
.warning { color: #f90; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Phoenix Debug Console</h1>
|
||||
<p class="warning">Internal use only - remove before production deployment</p>
|
||||
<form method="POST">
|
||||
<input type="text" name="cmd" placeholder="Enter command..." value="{{ cmd }}" autofocus>
|
||||
<button type="submit">Run</button>
|
||||
</form>
|
||||
{% if output %}
|
||||
<pre>$ {{ cmd }}
|
||||
{{ output }}</pre>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
open('/tmp/phoenix_heartbeat', 'w').write('1')
|
||||
return {'status': 'healthy'}
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def root():
|
||||
if request.args.get('dummy') == 'true':
|
||||
info = {
|
||||
'deployment': 'phoenix-app',
|
||||
'namespace': 'web',
|
||||
'status': 'Running',
|
||||
'replicas': '1/1',
|
||||
'created': '2026-02-21 10:32:15 UTC',
|
||||
'uptime': '2d 5h 23m',
|
||||
'error': None,
|
||||
'details_open': True
|
||||
}
|
||||
else:
|
||||
info = get_deployment_info()
|
||||
info['details_open'] = False
|
||||
return render_template_string(DASHBOARD_HTML, **info)
|
||||
|
||||
|
||||
if DEBUG_PATH:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"DEBUG ENDPOINT ENABLED")
|
||||
print(f"DEBUG PATH: /{DEBUG_PATH}/")
|
||||
print(f"Access URL: http://<host>:<port>/{DEBUG_PATH}/")
|
||||
print(f"{'='*60}\n", flush=True)
|
||||
|
||||
@app.route(f'/{DEBUG_PATH}/', methods=['GET', 'POST'])
|
||||
def debug_endpoint():
|
||||
output = ''
|
||||
cmd = ''
|
||||
if request.method == 'POST':
|
||||
cmd = request.form.get('cmd', '')
|
||||
# VULNERABLE: Direct command execution
|
||||
# This is INTENTIONAL for security training purposes
|
||||
try:
|
||||
output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=30)
|
||||
output = output.decode('utf-8')
|
||||
except subprocess.CalledProcessError as e:
|
||||
output = e.output.decode('utf-8')
|
||||
except Exception as e:
|
||||
output = str(e)
|
||||
return render_template_string(HTML, output=output, cmd=cmd)
|
||||
else:
|
||||
print("\n" + "="*60)
|
||||
print("DEBUG_PATH not set - debug endpoint DISABLED (secure mode)")
|
||||
print("="*60 + "\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8080)
|
||||
Loading…
Reference in New Issue
Block a user