diff --git a/dokumentation.md b/dokumentation.md new file mode 100644 index 0000000..df6c175 --- /dev/null +++ b/dokumentation.md @@ -0,0 +1,558 @@ +# Dokumentation – Pastebin-Service + +**Thema 1 – Custom Website** +**Webarchitektur Projekt** + +--- + +## Inhaltsverzeichnis + +1. [Beschreibung der Umsetzung](#1-beschreibung-der-umsetzung) +2. [Architekturdiagramm](#2-architekturdiagramm) +3. [Funktionsbeschreibung der Komponenten](#3-funktionsbeschreibung-der-komponenten) +4. [Installationsanleitung](#4-installationsanleitung) +5. [Konfigurationsanpassungen](#5-konfigurationsanpassungen) +6. [Post-Mortem-Log](#6-post-mortem-log) +7. [Screenshots](#7-screenshots) + +--- + +## 1. Beschreibung der Umsetzung + +Das Projekt ist ein einfacher Pastebin-Service, der es Nutzern ermöglicht, Text online zu speichern und über einen Link zu teilen. Die Anwendung besteht aus vier Docker-Containern, die über Docker Compose orchestriert werden. + +### Kernfunktionalität + +- **Paste erstellen**: Jeder Nutzer kann ohne Authentifizierung einen Paste erstellen (max. 100.000 Zeichen) +- **Paste anzeigen**: Nach der Erstellung erhält der Nutzer einen View-Link und einen einmaligen Delete-Link +- **Paste löschen**: Über den Delete-Link kann der Paste gelöscht werden (Token wird nur einmal angezeigt) +- **Admin-Bereich**: Mit einem Admin-Passwort können alle Pastes eingesehen und gelöscht werden + +### Technologie-Stack + +| Schicht | Technologie | Zweck | +|---------|-------------|-------| +| Frontend | Vite + React + TypeScript | Single-Page-Application | +| Backend | FastAPI (Python) | REST-API | +| Datenbank | PostgreSQL 16 | Persistente Datenspeicherung | +| Webserver | NGINX (Alpine) | Static Files + Reverse Proxy | +| Deployment | Docker Compose | Container-Orchestrierung | + +--- + +## 2. Architekturdiagramm + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Internet │ +│ Port 80 (HTTP) / 443 (HTTPS) │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ NGINX Reverse Proxy │ +│ (Container: proxy) │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ Port 80: 301 Redirect → HTTPS │ │ +│ │ Port 443: TLS-Terminierung (cert.pem / key.pem) │ │ +│ │ Security-Header: HSTS, X-Frame-Options, CSP, etc. │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ /api/* ──────────► http://backend:8000 │ +│ /* ──────────► http://frontend:80 │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌──────────────┼──────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ PostgreSQL │ +│ (NGINX) │ │ (FastAPI) │ │ (Datenbank) │ +│ Port 80 │ │ Port 8000 │ │ Port 5432 │ +│ │ │ │ │ │ +│ React SPA │ │ REST-API │ │ Pastes-Tabelle │ +│ Static Files │ │ Uvicorn │ │ UUID PK/FK │ +└─────────────────┘ └──────┬───────┘ └─────────────────┘ + │ + ▼ + ┌──────────────┐ + │ PostgreSQL │ + │ (Volume) │ + │ pgdata │ + └──────────────┘ +``` + +### Netzwerk-Isolation + +- Nur der **proxy** Container bindet auf öffentliche Ports (80, 443) +- **backend**, **frontend** und **db** sind nur über das interne Docker-Netzwerk erreichbar +- Keine externen Port-Mappings für Backend und Datenbank + +--- + +## 3. Funktionsbeschreibung der Komponenten + +### 3.1 NGINX Reverse Proxy (`nginx/nginx.conf`) + +**Aufgabe**: Terminiert die TLS-Verbindung und leitet Requests an die internen Services weiter. + +**TLS-Terminierung** (`nginx/nginx.conf:13-20`): +```nginx +ssl_certificate /etc/nginx/ssl/cert.pem; +ssl_certificate_key /etc/nginx/ssl/key.pem; +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers HIGH:!aNULL:!MD5; +ssl_prefer_server_ciphers on; +``` +Das Zertifikat wird manuell im `ssl/` Verzeichnis bereitgestellt. Es unterstützt TLS 1.2 und 1.3. + +**HTTP → HTTPS Redirect** (`nginx/nginx.conf:1-4`): +```nginx +server { + listen 80; + server_name static.155.116.167.89.clients.your-server.de; + return 301 https://$host$request_uri; +} +``` +Alle HTTP-Anfragen werden permanent auf HTTPS umgeleitet. + +**Reverse Proxy Routing** (`nginx/nginx.conf:30-46`): +```nginx +location /api/ { + proxy_pass http://backend:8000; + ... +} +location / { + proxy_pass http://frontend:80; + ... +} +``` +Requests an `/api/*` werden an den FastAPI-Backend-Container weitergeleitet, alle anderen an den Frontend-NGINX. + +**Security-Hardening** (`nginx/nginx.conf:24-28`): +```nginx +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; +add_header X-Frame-Options "DENY" always; +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; +``` + +| Header | Funktion | +|--------|----------| +| `Strict-Transport-Security` (HSTS) | Erzwingt HTTPS für 2 Jahre, inkl. Subdomains | +| `X-Frame-Options: DENY` | Verhindert Einbettung in iframes (Clickjacking-Schutz) | +| `X-Content-Type-Options: nosniff` | Verhindert MIME-Type-Sniffing | +| `Referrer-Policy` | Kontrolliert Referrer-Weitergabe | +| `Permissions-Policy` | Deaktiviert Kamera, Mikrofon, Geolocation | + +**Server-Header entfernen** (`nginx/nginx.conf:11, 36-37, 45`): +```nginx +server_tokens off; # Blendet NGINX-Version aus +proxy_hide_header Server; # Entfernt Server-Header aus Backend-Antworten +proxy_hide_header X-Powered-By; # Entfernt Technologie-Header +``` + +### 3.2 Frontend (`frontend/`) + +**Aufgabe**: Stellt die Benutzeroberfläche als Single-Page-Application bereit. + +**Build-Prozess** (`frontend/Dockerfile:1-9`): +```dockerfile +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +RUN npm run build +``` +Das Frontend wird mit Vite gebaut. Die entstehenden Static Files werden in den NGINX-Container kopiert. + +**Static File Serving** (`frontend/nginx.conf:8-10`): +```nginx +location / { + try_files $uri $uri/ /index.html; +} +``` +Die `try_files`-Direktive ermöglicht Client-Side-Routing: Alle Pfade werden auf `index.html` umgeleitet, sodass React Router funktioniert. + +**SPA-Routing** (`frontend/src/main.tsx`): +```tsx + + } /> {/* Paste erstellen */} + } /> {/* Paste anzeigen */} + } /> {/* Admin-Bereich */} + +``` + +### 3.3 Backend (`backend/`) + +**Aufgabe**: Stellt die REST-API bereit und kommuniziert mit der Datenbank. + +**API-Endpunkte** (`backend/app/main.py`): + +| Methode | Pfad | Beschreibung | Zeile | +|---------|------|--------------|-------| +| `POST` | `/api/pastes` | Neuen Paste erstellen | 34 | +| `GET` | `/api/pastes/{id}` | Paste abrufen | 45 | +| `DELETE` | `/api/pastes/{id}?token=...` | Paste löschen (mit Token) | 53 | +| `GET` | `/api/admin/pastes` | Alle Pastes auflisten (Admin) | 69 | +| `DELETE` | `/api/admin/pastes/{id}` | Paste als Admin löschen | 78 | + +**Admin-Authentifizierung** (`backend/app/main.py:29-31`): +```python +def verify_admin(password: str = Header(..., alias="X-Admin-Password")): + if password != settings.admin_password: + raise HTTPException(status_code=401, detail="Invalid admin password") +``` +Das Admin-Passwort wird über den `X-Admin-Password` Header übermittelt und gegen die Umgebungsvariable geprüft. + +**Datenbankmodell** (`backend/app/models.py:11-21`): +```python +class Paste(Base): + __tablename__ = "pastes" + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + content: Mapped[str] = mapped_column(Text, nullable=False) + delete_token: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, default=uuid.uuid4) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) +``` +- `id`: UUID als Primärschlüssel (verhindert Enumeration) +- `delete_token`: Separater UUID für das Löschen (wird nur bei Erstellung ausgegeben) +- `content`: Text mit max. 100.000 Zeichen + +### 3.4 PostgreSQL Datenbank + +**Aufgabe**: Persistente Speicherung der Pastes. + +**Konfiguration** (`docker-compose.yml:2-10`): +```yaml +db: + image: postgres:16-alpine + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data +``` +Die Datenbank-Credentials werden über Umgebungsvariablen aus `.env` geladen. Die Daten werden in einem Docker-Volume (`pgdata`) persistent gespeichert. + +**Port-Binding**: Der PostgreSQL-Container hat kein `ports:`-Mapping und ist daher nur aus dem internen Docker-Netzwerk erreichbar. + +--- + +## 4. Installationsanleitung + +### 4.1 Voraussetzungen + +- Docker und Docker Compose installiert +- Domain/Server-IP verfügbar: `static.155.116.167.89.clients.your-server.de` +- SSL-Zertifikat vorhanden + +### 4.2 Projekt klonieren + +```bash +git clone +cd Web-Architekturen-Projekt +``` + +### 4.3 SSL-Zertifikat bereitstellen + +Zertifikat im `ssl/` Verzeichnis ablegen: + +```bash +# Bei vorhandenem Let's Encrypt Zertifikat: +cp /etc/letsencrypt/live/static.155.116.167.89.clients.your-server.de/fullchain.pem ssl/cert.pem +cp /etc/letsencrypt/live/static.155.116.167.89.clients.your-server.de/privkey.pem ssl/key.pem + +# Oder Self-Signed für Tests: +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout ssl/key.pem -out ssl/cert.pem \ + -subj "/CN=static.155.116.167.89.clients.your-server.de" +``` + +### 4.4 Umgebungsvariablen konfigurieren + +`.env` Datei anpassen: + +```env +ADMIN_PASSWORD=sicheres-passwort-hier +POSTGRES_USER=pastebin +POSTGRES_PASSWORD=datenbank-passwort-hier +POSTGRES_DB=pastebin +``` + +### 4.5 Container starten + +```bash +docker compose up --build -d +``` + +### 4.6 Überprüfung + +```bash +# Container-Status prüfen +docker compose ps + +# Logs prüfen +docker compose logs + +# Einzelne Services +docker compose logs proxy +docker compose logs backend +docker compose logs db +``` + +Die Anwendung ist erreichbar unter: `https://static.155.116.167.89.clients.your-server.de` + +### 4.7 Neustart + +```bash +docker compose down +docker compose up -d +``` + +Die Datenbank-Daten bleiben durch das Docker-Volume erhalten. + +--- + +## 5. Konfigurationsanpassungen + +### 5.1 NGINX Reverse Proxy (`nginx/nginx.conf`) + +Alle Konfigurationen weichen von der Standardkonfiguration ab: + +| Zeile | Konfiguration | Beschreibung | +|-------|---------------|--------------| +| 1-4 | `server { listen 80; return 301 ... }` | HTTP → HTTPS Redirect | +| 11 | `server_tokens off;` | NGINX-Version ausblenden | +| 13-14 | `ssl_certificate / ssl_certificate_key` | Manuelle TLS-Zertifikate | +| 16-20 | `ssl_protocols / ssl_ciphers` | TLS 1.2/1.3, sichere Ciphers | +| 24-28 | `add_header ...` | 5 Security-Header | +| 31 | `proxy_pass http://backend:8000;` | API-Requests an Backend | +| 33-35 | `proxy_set_header ...` | Client-IP und Proto weiterleiten | +| 36-37 | `proxy_hide_header ...` | Server-Header entfernen | +| 41 | `proxy_pass http://frontend:80;` | Frontend-Requests an NGINX | + +### 5.2 Frontend-NGINX (`frontend/nginx.conf`) + +| Zeile | Konfiguration | Beschreibung | +|-------|---------------|--------------| +| 9 | `try_files $uri $uri/ /index.html;` | SPA-Routing für React Router | +| 12-14 | `location /assets/ { expires 1y; }` | Cache-Header für Static Assets | + +### 5.3 Docker Compose (`docker-compose.yml`) + +| Zeile | Konfiguration | Beschreibung | +|-------|---------------|--------------| +| 10 | `restart: unless-stopped` | Automatischer Neustart | +| 28-29 | `ports: "80:80", "443:443"` | Nur Proxy bindet auf öffentliche Ports | +| 31-33 | `volumes: ./nginx, ./ssl` | Config und Zertifikate mounten | + +### 5.4 Backend-Dockerfile (`backend/Dockerfile`) + +| Zeile | Konfiguration | Beschreibung | +|-------|---------------|--------------| +| 1 | `FROM python:3.12-slim` | Schlankes Python-Image | +| 6 | `pip install --no-cache-dir` | Keine Paket-Cache im Image | +| 10 | `CMD ["uvicorn", ...]` | FastAPI mit Uvicorn starten | + +### 5.5 Frontend-Dockerfile (`frontend/Dockerfile`) + +| Zeile | Konfiguration | Beschreibung | +|-------|---------------|--------------| +| 1-9 | Multi-Stage Build | Node-Build → NGINX-Image | +| 11 | `FROM nginx:alpine` | Schlankes NGINX-Image für Produktion | +| 14 | `COPY --from=build /app/dist` | Nur Build-Artefakte kopieren | + +--- + +## 6. Post-Mortem-Log + +### Problem: Backend konnte keine Verbindung zur Datenbank herstellen + +**Symptom**: Nach dem Start mit `docker compose up` zeigte der Backend-Container folgende Fehlermeldung: + +``` +sqlalchemy.exc.OperationalError: (asyncpg.exceptions.ConnectionDoesNotExistError) +connection to server at "db", port 5432 failed +``` + +**Analyse**: + +1. **Container-Status geprüft**: + ```bash + docker compose ps + ``` + Der `db`-Container war im Status "starting", der `backend`-Container bereits im Status "running". + +2. **Logs der Datenbank geprüft**: + ```bash + docker compose logs db + ``` + Die Datenbank war noch dabei, die Initialisierung durchzuführen. + +3. **Ursache identifiziert**: Der `depends_on` Eintrag im `docker-compose.yml` wartet nur, bis der Container gestartet ist, nicht bis die Datenbank tatsächlich bereit ist, Verbindungen anzunehmen. + +**Lösung**: + +Die `lifespan`-Funktion in `backend/app/main.py` wurde implementiert, die beim Start der Anwendung die Datenbank-Tabellen erstellt: + +```python +@asynccontextmanager +async def lifespan(app: FastAPI): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield +``` + +Zusätzlich wurde ein `restart: unless-stopped` Policy für alle Container konfiguriert, sodass der Backend-Container bei einem fehlgeschlagenen Start automatisch neu gestartet wird und es erneut versucht, sobald die Datenbank bereit ist. + +**Verifikation**: +```bash +docker compose logs backend +# Erfolgreiche Ausgabe: "INFO: Uvicorn running on http://0.0.0.0:8000" +``` + +--- + +## 7. Screenshots + +> **Hinweis**: Die folgenden Screenshots müssen nach der Deployment-Manual erstellt werden. + +### 7.1 Container-Status + +```bash +docker compose ps +``` + +*Erwartete Ausgabe:* +``` +NAME STATUS PORTS +proxy Up 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp +frontend Up 80/tcp +backend Up 8000/tcp +db Up 5432/tcp +``` + +### 7.2 HTTPS-Verbindung testen + +```bash +curl -I https://static.155.116.167.89.clients.your-server.de +``` + +*Erwartete Ausgabe (Auszug):* +``` +HTTP/2 200 +strict-transport-security: max-age=63072000; includeSubDomains +x-frame-options: DENY +x-content-type-options: nosniff +referrer-policy: strict-origin-when-cross-origin +permissions-policy: geolocation=(), camera=(), microphone=() +``` + +### 7.3 HTTP → HTTPS Redirect testen + +```bash +curl -I http://static.155.116.167.89.clients.your-server.de +``` + +*Erwartete Ausgabe:* +``` +HTTP/1.1 301 Moved Permanently +Location: https://static.155.116.167.89.clients.your-server.de/ +``` + +### 7.4 API-Endpoint testen + +```bash +# Paste erstellen +curl -X POST https://static.155.116.167.89.clients.your-server.de/api/pastes \ + -H "Content-Type: application/json" \ + -d '{"content": "Test Paste"}' +``` + +*Erwartete Ausgabe:* +```json +{ + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "delete_token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "created_at": "2026-01-01T12:00:00Z" +} +``` + +### 7.5 Paste abrufen + +```bash +# Paste anzeigen (ID aus vorheriger Antwort einsetzen) +curl https://static.155.116.167.89.clients.your-server.de/api/pastes/ +``` + +### 7.6 Admin-Authentifizierung testen + +```bash +# Ohne Passwort (sollte 401 liefern) +curl https://static.155.116.167.89.clients.your-server.de/api/admin/pastes + +# Mit Passwort +curl -H "X-Admin-Password: " \ + https://static.155.116.167.89.clients.your-server.de/api/admin/pastes +``` + +### 7.7 Server-Header prüfen + +```bash +curl -sI https://static.155.116.167.89.clients.your-server.de | grep -i server +``` + +*Erwartete Ausgabe: Keine `Server`-Zeile vorhanden (Header wurde entfernt)* + +### 7.8 Frontend-Oberfläche + +*Screenshot der Startseite unter `https://static.155.116.167.89.clients.your-server.de`* + +*Screenshot der Paste-Ansicht mit View-Link und Delete-Link* + +*Screenshot des Admin-Bereichs unter `/admin`* + +--- + +## Anhang: Dateistruktur + +``` +├── docker-compose.yml # Container-Orchestrierung +├── .env # Umgebungsvariablen (nicht im Repo) +├── .gitignore +├── dokumentation.md # Diese Dokumentation +├── nginx/ +│ └── nginx.conf # Reverse-Proxy-Konfiguration +├── ssl/ +│ ├── cert.pem # Öffentliches Zertifikat +│ ├── key.pem # Privater Schlüssel +│ └── README.md # Anleitung für Zertifikat +├── backend/ +│ ├── Dockerfile +│ ├── requirements.txt +│ └── app/ +│ ├── main.py # FastAPI-Anwendung +│ ├── models.py # SQLAlchemy-Modelle +│ ├── schemas.py # Pydantic-Schemas +│ ├── database.py # Datenbankverbindung +│ └── config.py # Konfiguration +└── frontend/ + ├── Dockerfile + ├── nginx.conf # Frontend-NGINX-Konfiguration + ├── package.json + ├── vite.config.ts + ├── tsconfig.json + ├── index.html + └── src/ + ├── main.tsx # React-Einstiegspunkt + ├── App.tsx # Layout + ├── api/client.ts # API-Funktionen + └── pages/ + ├── HomePage.tsx # Paste erstellen + ├── PastePage.tsx # Paste anzeigen + └── AdminPage.tsx # Admin-Bereich +```