Option C: Custom Hardened RTMP Solution

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:

  1. Fork von aau-zid/BigBlueButton-liveStreaming
  2. Fix BBB 3.0 UI Kompatibilität
  3. Security Hardening
  4. 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 host mode)
  • [ ] 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:

  1. âś… Security requirements understood?
  2. âś… 4-5 days development time available?
  3. âś… Maintenance commitment (2h/month)?
  4. âť“ More than 5 episodes planned? (ROI positive)
  5. âť“ Need automation? (vs manual OBS)

If ANY answer is No → Stick with Manual OBS Browser Source


See Also:


Created: 2025-12-08 Status: Draft Next Review: After Episode 02 (17.12.2025)