Option C: Custom Hardened RTMP Streaming Solution
Status: đź“‹ Planning Target: Production-ready fĂĽr Episode 03+ Effort: 4-5 days Security: High (Audited & Hardened)
Goal
Erstelle eine sichere, BBB 3.0-kompatible RTMP Streaming Lösung durch:
- Fork von aau-zid/BigBlueButton-liveStreaming
- Fix BBB 3.0 UI Kompatibilität
- Security Hardening
- Production Deployment
For complete analysis of why this is needed, see BBB 3.0 RTMP Findings.
Phase 1: Fork & Fix (1-2 days)
1.1 Repository Setup
# Fork erstellen
gh repo fork aau-zid/BigBlueButton-liveStreaming --clone
cd BigBlueButton-liveStreaming
git checkout -b bbb-3.0-compatibility
# Branches
# - main: Upstream tracking
# - bbb-3.0-compatibility: Unsere Fixes
# - production: Production releases
1.2 UI Compatibility Fix
Problem: stream.py Line 144 - Chat Window Timeout
Current Code:
def bbb_browser():
# ...
logger.info("Waiting for chat input window to appear.")
element = EC.presence_of_element_located((By.CSS_SELECTOR, 'textarea[id="message-input"]'))
WebDriverWait(browser, selenium_timeout).until(element)
# TIMEOUT HERE with BBB 3.0!
Fix Option A: Skip Chat (Quick)
def bbb_browser():
# ...
# Skip chat message if BBB_CHAT_MESSAGE is empty
if os.getenv('BBB_CHAT_MESSAGE'):
logger.info("Waiting for chat input window to appear.")
try:
element = EC.presence_of_element_located((By.CSS_SELECTOR, 'textarea[id="message-input"]'))
WebDriverWait(browser, 10).until(element) # Shorter timeout
# Post message
except TimeoutException:
logger.warning("Chat window not found, continuing without message")
else:
logger.info("Skipping chat message (BBB_CHAT_MESSAGE not set)")
Fix Option B: BBB 3.0 Selector (Proper)
def bbb_browser():
# ...
# Try multiple selectors for different BBB versions
chat_selectors = [
'textarea[id="message-input"]', # BBB 2.x
'textarea[data-test="chatInput"]', # BBB 3.0
'textarea[aria-label="Chat message input"]', # Generic
]
for selector in chat_selectors:
try:
element = EC.presence_of_element_located((By.CSS_SELECTOR, selector))
WebDriverWait(browser, 5).until(element)
logger.info(f"Found chat input using selector: {selector}")
break
except TimeoutException:
continue
else:
logger.warning("No chat input found, skipping message")
Testing:
# Build test image
docker build -t bbb-streaming-bbb3:test .
# Test with BBB 3.0
docker run --rm \
-e BBB_URL=https://bbb.foss.systems/bigbluebutton/api \
-e BBB_SECRET=xxx \
-e BBB_MEETING_ID=xxx \
-e BBB_STREAM_URL=rtmp://localhost:1935/live/test \
--network host \
bbb-streaming-bbb3:test
1.3 Additional BBB 3.0 Fixes
Check andere UI Interaktionen:
# Mögliche weitere Probleme:
# - Audio join button
# - Video controls
# - Presentation area
# - Layout elements
Adaptive Strategy:
def wait_for_element_adaptive(browser, selectors, timeout=30, description="element"):
"""
Try multiple selectors with shorter individual timeouts.
BBB 3.0 compatible.
"""
for selector in selectors:
try:
element = EC.presence_of_element_located((By.CSS_SELECTOR, selector))
WebDriverWait(browser, timeout // len(selectors)).until(element)
logger.info(f"{description} found: {selector}")
return True
except TimeoutException:
logger.debug(f"{description} not found with selector: {selector}")
continue
logger.error(f"{description} not found with any selector")
return False
Phase 2: Security Hardening (1-2 days)
2.1 Credential Management
Problem: BBB_SECRET in plaintext .env
Fix: Secret Management
# docker-compose.yml
services:
bbb-streamer:
secrets:
- bbb_secret
environment:
- BBB_SECRET_FILE=/run/secrets/bbb_secret
secrets:
bbb_secret:
file: ./secrets/bbb_secret.txt # Outside git
Alternative: Per-Meeting Passwords
# Statt BBB_SECRET:
BBB_ATTENDEE_PASSWORD=meeting-specific-password
# stream.py anpassen:
# join_url mit attendee password statt BBB_SECRET
2.2 Container Hardening
Dockerfile Security:
FROM ubuntu:22.04
# Run as non-root
RUN useradd -m -u 1000 streamer
# Minimize attack surface
RUN apt-get update && apt-get install -y \
xvfb \
google-chrome-stable \
ffmpeg \
python3 \
&& rm -rf /var/lib/apt/lists/*
# Drop capabilities
USER streamer
WORKDIR /home/streamer
# Read-only root filesystem
# (Writeable dirs via tmpfs/volumes)
docker-compose.yml Hardening:
services:
bbb-streamer:
image: bbb-streaming-hardened:latest
# Security options
security_opt:
- no-new-privileges:true
- seccomp:unconfined # Chrome needs this
- apparmor:unconfined # Chrome needs this
# Capabilities (drop all, add only needed)
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # If needed
- SYS_ADMIN # Chrome sandbox needs this
# Read-only root
read_only: true
tmpfs:
- /tmp
- /home/streamer/.cache
- /dev/shm:size=2g # Chrome shared memory
# Network isolation
networks:
- bbb_streaming_net
# Resource limits
mem_limit: 4g
cpus: 2.0
# No host mode!
# dns:
# - 8.8.8.8
networks:
bbb_streaming_net:
driver: bridge
internal: false # Needs internet for BBB
2.3 Network Security
Firewall Rules:
# /opt/BigBlueButton-liveStreaming/firewall.sh
# Allow ONLY to BBB server
iptables -A OUTPUT -p tcp -d 78.47.61.166 --dport 443 -j ACCEPT # BBB HTTPS
# Allow ONLY to nginx-rtmp (localhost)
iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport 1935 -j ACCEPT # RTMP
# Allow DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
# Drop everything else
iptables -A OUTPUT -j DROP
RTMP URL Validation:
# stream.py
import urllib.parse
def validate_rtmp_url(url):
"""
Only allow RTMP to trusted servers.
"""
allowed_hosts = [
'localhost',
'127.0.0.1',
'rtmp.liveplay.studio', # Your RTMP server
]
parsed = urllib.parse.urlparse(url)
if parsed.scheme != 'rtmp':
raise ValueError(f"Only RTMP protocol allowed, got: {parsed.scheme}")
if parsed.hostname not in allowed_hosts:
raise ValueError(f"RTMP host not allowed: {parsed.hostname}")
return True
# In main():
rtmp_url = os.getenv('BBB_STREAM_URL')
if not validate_rtmp_url(rtmp_url):
sys.exit(1)
2.4 Access Control & Audit Logging
API Wrapper (control.py):
#!/usr/bin/env python3
"""
Control API for streaming.
Requires authentication.
"""
from flask import Flask, request, jsonify
import subprocess
import jwt
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
SECRET_KEY = open('/run/secrets/api_secret').read().strip()
def verify_token(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.InvalidTokenError:
return None
@app.route('/stream/start', methods=['POST'])
def start_stream():
# Verify token
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user = verify_token(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
# Validate input
meeting_id = request.json.get('meeting_id')
if not meeting_id:
return jsonify({'error': 'meeting_id required'}), 400
# Audit log
logging.info(f"Stream started by {user['username']} for meeting {meeting_id}")
# Start container
subprocess.run([
'docker', 'compose', 'up', '-d',
'--env', f'BBB_MEETING_ID={meeting_id}'
])
return jsonify({'status': 'started', 'meeting_id': meeting_id})
@app.route('/stream/stop', methods=['POST'])
def stop_stream():
# Similar with auth & logging
pass
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000) # Localhost only!
Nginx Reverse Proxy (mit Auth):
# /etc/nginx/sites-available/streaming-api
server {
listen 443 ssl;
server_name streaming-api.liveplay.studio;
ssl_certificate /etc/letsencrypt/live/streaming-api.liveplay.studio/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/streaming-api.liveplay.studio/privkey.pem;
# Rate limiting
limit_req_zone $binary_remote_addr zone=streaming_api:10m rate=10r/m;
limit_req zone=streaming_api burst=5;
location / {
proxy_pass http://127.0.0.1:5000;
# IP Whitelist
allow 192.168.1.0/24; # Internal network
deny all;
}
}
2.5 Monitoring & Alerting
Prometheus Metrics:
# metrics.py
from prometheus_client import Counter, Gauge, start_http_server
streams_started = Counter('streams_started_total', 'Total streams started')
streams_active = Gauge('streams_active', 'Currently active streams')
stream_errors = Counter('stream_errors_total', 'Stream errors', ['error_type'])
# In stream.py:
streams_started.inc()
streams_active.inc()
try:
# ... streaming logic
except Exception as e:
stream_errors.labels(error_type=type(e).__name__).inc()
raise
finally:
streams_active.dec()
Health Check Endpoint:
@app.route('/health')
def health():
# Check if container is running
result = subprocess.run(['docker', 'ps', '--filter', 'name=liveStreaming'],
capture_output=True)
running = 'liveStreaming' in result.stdout.decode()
return jsonify({
'status': 'healthy' if running else 'unhealthy',
'container_running': running
})
Phase 3: Testing (1 day)
3.1 Unit Tests
# tests/test_ui_selectors.py
import pytest
from unittest.mock import Mock, patch
def test_chat_window_adaptive_selectors():
"""Test that adaptive selectors work with multiple BBB versions."""
mock_browser = Mock()
# Test BBB 2.x selector
assert wait_for_element_adaptive(
mock_browser,
['textarea[id="message-input"]'],
description="chat"
)
# Test BBB 3.0 selector
assert wait_for_element_adaptive(
mock_browser,
['textarea[data-test="chatInput"]'],
description="chat"
)
3.2 Integration Tests
#!/bin/bash
# tests/integration_test.sh
set -e
echo "Starting test BBB meeting..."
# Create test meeting via BBB API
echo "Starting streaming container..."
docker compose -f docker-compose.test.yml up -d
echo "Waiting for stream to start..."
sleep 30
echo "Checking RTMP stream..."
curl -f http://localhost:8080/live/test.m3u8 || exit 1
echo "Checking stream quality..."
ffprobe http://localhost:8080/live/test.m3u8 2>&1 | grep "1920x1080"
echo "âś… All tests passed"
docker compose -f docker-compose.test.yml down
3.3 Security Tests
# tests/security_test.sh
echo "Testing container escape attempts..."
docker exec liveStreaming bash -c "
# Try to access host files
! cat /proc/1/environ 2>/dev/null || exit 1
# Try to escalate privileges
! sudo -l 2>/dev/null || exit 1
# Try to access Docker socket
! ls /var/run/docker.sock 2>/dev/null || exit 1
"
echo "Testing network isolation..."
docker exec liveStreaming bash -c "
# Should NOT be able to connect to random internet
! curl -m 5 http://example.com 2>/dev/null || exit 1
# Should be able to connect to BBB
curl -m 5 https://bbb.foss.systems/ >/dev/null || exit 1
"
echo "âś… Security tests passed"
Phase 4: Production Deployment (1 day)
4.1 Production Checklist
- [ ] Code Review (Security-focused)
- [ ] All tests passing
- [ ] Docker image scanned (
docker scan,trivy) - [ ] Secrets in place (not in git!)
- [ ] Firewall rules configured
- [ ] Monitoring/Alerting configured
- [ ] Documentation updated
- [ ] Rollback plan ready
- [ ] Incident response plan
4.2 Deployment Steps
# 1. Build production image
docker build -t bbb-streaming-hardened:1.0.0 .
docker tag bbb-streaming-hardened:1.0.0 bbb-streaming-hardened:latest
# 2. Push to private registry (optional)
docker tag bbb-streaming-hardened:1.0.0 registry.liveplay.studio/bbb-streaming:1.0.0
docker push registry.liveplay.studio/bbb-streaming:1.0.0
# 3. Deploy to production
cd /opt/BigBlueButton-liveStreaming-hardened
git pull
docker compose pull
docker compose up -d
# 4. Verify
docker compose ps
docker compose logs -f --tail=50
# 5. Test with real meeting
curl -X POST https://streaming-api.liveplay.studio/stream/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"meeting_id": "test-meeting-id"}'
4.3 Rollback Plan
# If production deployment fails:
# 1. Stop new version
docker compose down
# 2. Switch to previous version
docker compose -f docker-compose.yml.backup up -d
# 3. Or: Manual OBS fallback
# Inform operators to use OBS Browser Source
Timeline
| Phase | Task | Duration | Dependencies |
|---|---|---|---|
| Week 1 | Fork + UI Fix | 1-2 days | - |
| Security Hardening | 1-2 days | UI Fix done | |
| Week 2 | Testing | 1 day | Hardening done |
| Production Deploy | 1 day | Tests passed | |
| Week 3 | Monitoring | Ongoing | Deployed |
| Episode 03 Test | 17.12.2025 | Deployed |
Earliest Production: ~5 days from start Target: Ready before Episode 03
Success Criteria
- [ ] Works with BBB 3.0.16
- [ ] No BBB_SECRET in environment variables
- [ ] Container hardened (security audit passed)
- [ ] Network isolated (no
hostmode) - [ ] Access control implemented
- [ ] Audit logging active
- [ ] Monitoring/Alerting configured
- [ ] Documentation complete
- [ ] Tested with real meetings
- [ ] Zero security incidents in first week
Maintenance Plan
Weekly:
- Review audit logs
- Check for failed streams
- Monitor resource usage
Monthly:
- Update dependencies (npm, apt)
- Rebuild Docker images
- Security scan
- Review access tokens/passwords
Per-BBB-Update:
- Test UI compatibility
- Update selectors if needed
- Regression testing
Cost-Benefit Analysis
Costs:
- Development: 4-5 days effort
- Maintenance: ~2 hours/month
- Infrastructure: Minimal (already have server)
Benefits:
- âś… Automated RTMP streaming
- âś… Security compliant
- âś… Scalable to multiple meetings
- âś… Professional production workflow
- âś… Reusable for other projects
ROI: High (if >5 episodes/year)
Alternative Cost: Manual OBS = 15 min setup/episode = Free but not scalable
Decision Points
Before starting Option C, confirm:
- âś… Security requirements understood?
- âś… 4-5 days development time available?
- âś… Maintenance commitment (2h/month)?
- âť“ More than 5 episodes planned? (ROI positive)
- âť“ Need automation? (vs manual OBS)
If ANY answer is No → Stick with Manual OBS Browser Source
See Also:
- BBB 3.0 RTMP Findings - Root cause analysis and security audit
- Session Summary - Episode 01 session recap
- Deployment Guide - Current infrastructure status
Created: 2025-12-08 Status: Draft Next Review: After Episode 02 (17.12.2025)