- Host-based access control (public vs internal subdomain) - Backend middleware for API protection - Frontend code splitting for internal-only features - Rate limiting for public uploads (20/hour/IP) - Comprehensive testing strategy - Security review and deployment plan
33 KiB
Feature Plan: Public vs. Internal Frontend/API per Subdomain
Erstellt: 25.11.2025
Basiert auf: FEATURE_REQUEST-FrontendPublic.md
Ziel: Subdomain-abhängige Features und API-Zugriffe (Public Upload-Only vs. Internal Full-Feature)
1. Übersicht & Architektur-Entscheidungen
1.1 Ziele
- Public Host (
deinprojekt.hobbyhimmel.de): Nur Upload + Management-Portal (UUID-basiert) - Internal Host (
deinprojekt.lan.hobbyhimmel.de): Vollständige App (Slideshow, Groups, Moderation, Admin) - Sicherheit: Serverseitige Blockierung von Admin/Moderation/Groups APIs auf public Host
- Performance: Code Splitting - internal Features werden auf public Host nicht geladen
1.2 Architektur (bestätigt)
- Ein Docker Container mit einem Port (80 für Frontend, 5000 für Backend)
- nginx-proxy-manager leitet beide Subdomains auf denselben Container weiter
- Setzt automatisch
X-Forwarded-HostHeader - Public:
deinprojekt.hobbyhimmel.de→ Container:80 - Internal:
deinprojekt.lan.hobbyhimmel.de→ Container:80
- Setzt automatisch
- Backend: Erkennt Host via
X-Forwarded-Hostund blockiert geschützte APIs für public - Frontend:
- Ein Build mit React Code Splitting (lazy loading)
- Runtime-Erkennung der Subdomain
- Internal-only Routes werden auf public Host nicht geladen
1.3 Sicherheitskonzept
- Defense in Depth:
- Backend Middleware blockiert geschützte APIs basierend auf Host
- Frontend lädt internal Features nicht (Code Splitting)
- Rate Limiting für public Uploads (20/Stunde/IP)
- Geschützte Ressourcen (nur internal):
/api/admin/*- Admin-Funktionen/api/groups- Groups Listing/api/slideshow- Slideshow Data/api/migration/*- Migration Tools/api/moderation/*- Moderation (falls vorhanden)
- Public erlaubte Ressourcen:
/api/upload- Upload Endpoint/api/manage/:token- Management Portal (UUID-basiert)/api/previews/*- Preview Images (nur mit validem Token)
2. Environment Variablen
2.1 Neue Variablen
Backend (docker/prod/backend/.env bzw. docker-compose):
# Host Configuration
PUBLIC_HOST=deinprojekt.hobbyhimmel.de
INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
# Rate Limiting (Public Host)
PUBLIC_UPLOAD_RATE_LIMIT=20
PUBLIC_UPLOAD_RATE_WINDOW=3600000 # 1 Stunde in ms
# Feature Flags
ENABLE_HOST_RESTRICTION=true
Frontend (runtime env-config.js):
window._env_ = {
API_URL: process.env.API_URL || 'http://localhost:5000',
PUBLIC_HOST: process.env.PUBLIC_HOST || 'deinprojekt.hobbyhimmel.de',
INTERNAL_HOST: process.env.INTERNAL_HOST || 'deinprojekt.lan.hobbyhimmel.de'
};
2.2 docker-compose.yml Anpassungen
File: docker/prod/docker-compose.yml
backend:
environment:
# ... existing vars ...
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
- PUBLIC_UPLOAD_RATE_LIMIT=20
- ENABLE_HOST_RESTRICTION=true
frontend:
environment:
# ... existing vars ...
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
3. Backend Implementierung
3.1 Host Gate Middleware
Neue Datei: backend/src/middlewares/hostGate.js
Zweck: Erkennt public vs. internal Host und blockiert geschützte Routes für public
Implementierung:
/**
* Host Gate Middleware
* Blockiert geschützte API-Routen für public Host
* Erlaubt nur Upload + Management für public
*/
const PUBLIC_HOST = process.env.PUBLIC_HOST || 'deinprojekt.hobbyhimmel.de';
const INTERNAL_HOST = process.env.INTERNAL_HOST || 'deinprojekt.lan.hobbyhimmel.de';
const ENABLE_HOST_RESTRICTION = process.env.ENABLE_HOST_RESTRICTION !== 'false';
// Routes die NUR für internal Host erlaubt sind
const INTERNAL_ONLY_ROUTES = [
'/api/admin',
'/api/groups',
'/api/slideshow',
'/api/migration',
'/api/moderation',
'/api/reorder',
'/api/batch-upload',
'/api/social-media',
'/api/auth/login', // Admin Login nur internal
'/api/auth/logout',
'/api/auth/session'
];
// Routes die für public Host erlaubt sind
const PUBLIC_ALLOWED_ROUTES = [
'/api/upload',
'/api/manage',
'/api/previews',
'/api/consent'
];
const hostGate = (req, res, next) => {
// Feature disabled in dev/test
if (!ENABLE_HOST_RESTRICTION || process.env.NODE_ENV === 'test') {
req.isPublicHost = false;
req.isInternalHost = true;
return next();
}
// Get host from X-Forwarded-Host (nginx-proxy-manager) or Host header
const host = req.get('x-forwarded-host') || req.get('host') || '';
const hostname = host.split(':')[0]; // Remove port if present
// Determine if request is from public or internal host
req.isPublicHost = hostname === PUBLIC_HOST;
req.isInternalHost = hostname === INTERNAL_HOST || hostname === 'localhost';
// If public host, check if route is allowed
if (req.isPublicHost) {
const path = req.path;
// Check if route is internal-only
const isInternalOnly = INTERNAL_ONLY_ROUTES.some(route =>
path.startsWith(route)
);
if (isInternalOnly) {
console.warn(`🚫 Public host blocked access to: ${path} (Host: ${hostname})`);
return res.status(403).json({
error: 'Not available on public host',
message: 'This endpoint is only available on the internal network'
});
}
}
// Add audit log context
req.requestSource = req.isPublicHost ? 'public' : 'internal';
next();
};
module.exports = hostGate;
Integration in Middleware Stack:
File: backend/src/middlewares/index.js
const hostGate = require('./hostGate');
const applyMiddlewares = (app) => {
// ... existing middlewares (CORS, body-parser, etc.) ...
// Host Gate MUSS VOR den Routes kommen
app.use(hostGate);
// ... rest of middlewares ...
};
3.2 Rate Limiter Anpassung
File: backend/src/middlewares/rateLimiter.js
Anpassung: Strengere Limits für public Host Uploads
const rateLimit = require('express-rate-limit');
const PUBLIC_UPLOAD_RATE_LIMIT = parseInt(process.env.PUBLIC_UPLOAD_RATE_LIMIT || '20', 10);
const PUBLIC_UPLOAD_RATE_WINDOW = parseInt(process.env.PUBLIC_UPLOAD_RATE_WINDOW || '3600000', 10);
// Bestehende Limiter...
// Neuer Public Upload Limiter
const publicUploadLimiter = rateLimit({
windowMs: PUBLIC_UPLOAD_RATE_WINDOW,
max: PUBLIC_UPLOAD_RATE_LIMIT,
message: {
error: 'Too many uploads',
message: `Maximum ${PUBLIC_UPLOAD_RATE_LIMIT} uploads per hour allowed`
},
standardHeaders: true,
legacyHeaders: false,
// Nur für public Host anwenden
skip: (req) => !req.isPublicHost
});
module.exports = {
// ... existing limiters ...
publicUploadLimiter
};
Integration in Upload Route:
File: backend/src/routes/upload.js
const { publicUploadLimiter } = require('../middlewares/rateLimiter');
// Apply public upload limiter
router.post('/upload', publicUploadLimiter, uploadController.uploadImages);
3.3 Audit Log Erweiterung
File: backend/src/middlewares/auditLog.js
Anpassung: source_host in Audit Logs aufnehmen
const logManagementAction = (req, action, details = {}) => {
const auditEntry = {
// ... existing fields ...
source_host: req.get('x-forwarded-host') || req.get('host'),
source_type: req.requestSource || 'unknown',
// ... rest ...
};
// Log to DB...
};
4. Frontend Implementierung
4.1 Runtime Environment Config
File: frontend/public/env-config.js
Zweck: Wird beim Container-Start mit echten Env-Variablen befüllt
// This file is replaced at runtime by env.sh
window._env_ = {
API_URL: "${API_URL}",
PUBLIC_HOST: "${PUBLIC_HOST}",
INTERNAL_HOST: "${INTERNAL_HOST}"
};
File: frontend/env.sh (anpassen)
#!/bin/bash
# Inject runtime environment variables
cat <<EOF > /usr/share/nginx/html/env-config.js
window._env_ = {
API_URL: "${API_URL:-http://localhost:5000}",
PUBLIC_HOST: "${PUBLIC_HOST:-deinprojekt.hobbyhimmel.de}",
INTERNAL_HOST: "${INTERNAL_HOST:-deinprojekt.lan.hobbyhimmel.de}"
};
EOF
4.2 Host Detection Utility
Neue Datei: frontend/src/Utils/hostDetection.js
/**
* Erkennt, ob App auf public oder internal Host läuft
* Basiert auf window.location.hostname + env-config
*/
export const getHostConfig = () => {
const hostname = window.location.hostname;
const publicHost = window._env_?.PUBLIC_HOST || 'deinprojekt.hobbyhimmel.de';
const internalHost = window._env_?.INTERNAL_HOST || 'deinprojekt.lan.hobbyhimmel.de';
const isPublic = hostname === publicHost;
const isInternal = hostname === internalHost || hostname === 'localhost';
return {
hostname,
publicHost,
internalHost,
isPublic,
isInternal,
// Feature Flags
canAccessAdmin: isInternal,
canAccessSlideshow: isInternal,
canAccessGroups: isInternal,
canAccessModeration: isInternal,
canUpload: true, // Immer erlaubt
canManageByUUID: true // Immer erlaubt
};
};
export const isPublicHost = () => getHostConfig().isPublic;
export const isInternalHost = () => getHostConfig().isInternal;
4.3 App.js mit Code Splitting
File: frontend/src/App.js
Anpassung: Lazy Loading für internal-only Pages
import React, { lazy, Suspense } from 'react';
import './App.css';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AdminSessionProvider } from './contexts/AdminSessionContext.jsx';
import { getHostConfig } from './Utils/hostDetection.js';
// Always loaded (public + internal)
import MultiUploadPage from './Components/Pages/MultiUploadPage';
import ManagementPortalPage from './Components/Pages/ManagementPortalPage';
import NotFoundPage from './Components/Pages/404Page.js';
// Lazy loaded (internal only)
const SlideshowPage = lazy(() => import('./Components/Pages/SlideshowPage'));
const GroupsOverviewPage = lazy(() => import('./Components/Pages/GroupsOverviewPage'));
const PublicGroupImagesPage = lazy(() => import('./Components/Pages/PublicGroupImagesPage'));
const ModerationGroupsPage = lazy(() => import('./Components/Pages/ModerationGroupsPage'));
const ModerationGroupImagesPage = lazy(() => import('./Components/Pages/ModerationGroupImagesPage'));
// Protected Route Component
const ProtectedRoute = ({ children }) => {
const hostConfig = getHostConfig();
if (hostConfig.isPublic) {
// Redirect to upload page with message
return <Navigate to="/" replace />;
}
return children;
};
// Loading Fallback
const LoadingFallback = () => (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}>
<p>Loading...</p>
</div>
);
function App() {
const hostConfig = getHostConfig();
return (
<AdminSessionProvider>
<Router>
<Suspense fallback={<LoadingFallback />}>
<Routes>
{/* Public Routes - immer verfügbar */}
<Route path="/" element={<MultiUploadPage />} />
<Route path="/manage/:token" element={<ManagementPortalPage />} />
{/* Internal Only Routes */}
{hostConfig.isInternal && (
<>
<Route
path="/slideshow"
element={
<ProtectedRoute>
<SlideshowPage />
</ProtectedRoute>
}
/>
<Route
path="/groups/:groupId"
element={
<ProtectedRoute>
<PublicGroupImagesPage />
</ProtectedRoute>
}
/>
<Route
path="/groups"
element={
<ProtectedRoute>
<GroupsOverviewPage />
</ProtectedRoute>
}
/>
<Route
path="/moderation"
element={
<ProtectedRoute>
<ModerationGroupsPage />
</ProtectedRoute>
}
/>
<Route
path="/moderation/groups/:groupId"
element={
<ProtectedRoute>
<ModerationGroupImagesPage />
</ProtectedRoute>
}
/>
</>
)}
{/* 404 / Not Found */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</Router>
</AdminSessionProvider>
);
}
export default App;
4.4 Navigation / Menu Anpassung
Betrifft: frontend/src/Components/Pages/MultiUploadPage.js und andere Pages mit Navigation
Anpassung: Menü-Items nur anzeigen, wenn auf internal Host
import { getHostConfig } from '../../Utils/hostDetection';
const MultiUploadPage = () => {
const hostConfig = getHostConfig();
return (
<div>
{/* Nur auf internal Host Navigation anzeigen */}
{hostConfig.isInternal && (
<nav>
<a href="/slideshow">Slideshow</a>
<a href="/groups">Groups</a>
<a href="/moderation">Moderation</a>
</nav>
)}
{/* Upload Form - immer sichtbar */}
<UploadForm />
{/* Optional: Hinweis für public users */}
{hostConfig.isPublic && (
<div className="public-notice">
<p>Sie nutzen den öffentlichen Upload-Bereich.</p>
</div>
)}
</div>
);
};
Hinweis: Bestehende Navigation ist bereits so gestaltet, dass auf Upload-Seite keine Menüpunkte sichtbar sind. Diese Logik wird durch hostConfig verstärkt.
4.5 404 Page Anpassung
File: frontend/src/Components/Pages/404Page.js
Anpassung: Unterschiedliche Meldung für public vs. internal
import { getHostConfig } from '../../Utils/hostDetection';
const NotFoundPage = () => {
const hostConfig = getHostConfig();
return (
<div className="not-found">
<h1>404 - Seite nicht gefunden</h1>
{hostConfig.isPublic ? (
<>
<p>Diese Funktion ist nicht öffentlich verfügbar.</p>
<a href="/">Zurück zum Upload</a>
</>
) : (
<>
<p>Die angeforderte Seite existiert nicht.</p>
<a href="/">Zurück zur Startseite</a>
</>
)}
</div>
);
};
export default NotFoundPage;
5. nginx-proxy-manager Konfiguration
5.1 Wichtige Hinweise
- X-Forwarded-Host: Wird automatisch gesetzt - keine manuelle Konfiguration nötig
- SSL: Beide Hosts müssen gültige Zertifikate haben
- Proxy-Hosts: Zwei separate Einträge erstellen
5.2 Proxy Host Setup (GUI)
Public Host:
Domain Names: deinprojekt.hobbyhimmel.de
Scheme: http
Forward Hostname/IP: image-uploader-frontend (Docker Container Name)
Forward Port: 80
Cache Assets: Yes
Block Common Exploits: Yes
Websockets Support: No
SSL:
- Let's Encrypt Certificate
- Force SSL: Yes
- HTTP/2 Support: Yes
- HSTS Enabled: Yes
Internal Host:
Domain Names: deinprojekt.lan.hobbyhimmel.de
Scheme: http
Forward Hostname/IP: image-uploader-frontend (Docker Container Name)
Forward Port: 80
Cache Assets: Yes
Block Common Exploits: Yes
Websockets Support: No
SSL:
- Let's Encrypt DNS Challenge (für *.lan.hobbyhimmel.de)
- Force SSL: Yes
- HTTP/2 Support: Yes
5.3 Optional: Zusätzliche nginx-Sicherheit
Advanced Settings für Public Host (optional, Defense in Depth):
# Block admin routes on nginx level (zusätzlich zu Backend-Middleware)
location ~ ^/api/(admin|groups|slideshow|moderation|migration|reorder|batch-upload|social-media) {
return 403;
}
# Allow only upload and management
location ~ ^/api/(upload|manage|previews|consent) {
proxy_pass http://image-uploader-frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
Hinweis: Dies ist optional - die Backend-Middleware ist bereits ausreichend. nginx-Blockierung ist zusätzliche Sicherheitsebene.
6. Docker & Deployment
6.1 docker-compose.yml Finale Anpassungen
File: docker/prod/docker-compose.yml
services:
frontend:
container_name: image-uploader-frontend
image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-frontend:latest
ports:
- "80:80" # Beide Hosts auf selben Port
build:
context: ../../
dockerfile: docker/prod/frontend/Dockerfile
depends_on:
- backend
environment:
- API_URL=http://backend:5000
- CLIENT_URL=http://localhost
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
networks:
- npm-nw
- prod-internal
backend:
container_name: image-uploader-backend
image: gitea.lan.hobbyhimmel.de/hobbyhimmel/image-uploader-backend:latest
build:
context: ../../
dockerfile: docker/prod/backend/Dockerfile
ports:
- "5000:5000"
volumes:
- image_data:/usr/src/app/src/data
networks:
- prod-internal
environment:
- REMOVE_IMAGES=false
- NODE_ENV=production
- ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET}
- ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions
- ADMIN_SESSION_COOKIE_SECURE=true
# Host Configuration
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
- PUBLIC_UPLOAD_RATE_LIMIT=20
- PUBLIC_UPLOAD_RATE_WINDOW=3600000
- ENABLE_HOST_RESTRICTION=true
# Trust nginx-proxy-manager
- TRUST_PROXY_HOPS=1
volumes:
image_data:
networks:
npm-nw:
external: true
prod-internal:
driver: bridge
6.2 Frontend Dockerfile Anpassung
File: docker/prod/frontend/Dockerfile
Sicherstellen: env.sh wird beim Container-Start ausgeführt
# ... existing build steps ...
# Copy env.sh
COPY frontend/env.sh /docker-entrypoint.d/
# Make executable
RUN chmod +x /docker-entrypoint.d/env.sh
# ... rest ...
6.3 Dev Setup Anpassungen
File: docker/dev/docker-compose.yml
services:
frontend:
environment:
# ... existing ...
- PUBLIC_HOST=localhost
- INTERNAL_HOST=localhost
backend:
environment:
# ... existing ...
- PUBLIC_HOST=localhost
- INTERNAL_HOST=localhost
- ENABLE_HOST_RESTRICTION=false # Disabled in dev
Hinweis: In Dev sind beide Hosts auf localhost, Feature-Restriction ist deaktiviert für einfaches Testing.
6.4 Testing mit Dev-Setup
Um beide Modi im Dev zu testen:
Option 1 - /etc/hosts Einträge:
127.0.0.1 deinprojekt.hobbyhimmel.de
127.0.0.1 deinprojekt.lan.hobbyhimmel.de
Option 2 - Browser URL Parameter:
// In hostDetection.js
const urlParams = new URLSearchParams(window.location.search);
const forcedMode = urlParams.get('mode'); // ?mode=public oder ?mode=internal
if (forcedMode === 'public') {
return { ...config, isPublic: true, isInternal: false };
}
7. Testing
7.1 Backend Unit Tests
Neue Datei: backend/tests/unit/middlewares/hostGate.test.js
const hostGate = require('../../../src/middlewares/hostGate');
describe('Host Gate Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
get: jest.fn(),
path: '/api/admin/test'
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
process.env.ENABLE_HOST_RESTRICTION = 'true';
process.env.PUBLIC_HOST = 'public.example.com';
process.env.INTERNAL_HOST = 'internal.example.com';
});
test('blocks admin routes on public host', () => {
req.get.mockReturnValue('public.example.com');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(next).not.toHaveBeenCalled();
});
test('allows admin routes on internal host', () => {
req.get.mockReturnValue('internal.example.com');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
test('allows upload route on public host', () => {
req.path = '/api/upload';
req.get.mockReturnValue('public.example.com');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('respects X-Forwarded-Host header', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return 'public.example.com';
return null;
});
req.path = '/api/admin/test';
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
});
7.2 Backend Integration Tests
Neue Datei: backend/tests/api/hostRestriction.test.js
const request = require('supertest');
const { setupTestServer, cleanupTestServer } = require('../testServer');
describe('Host Restriction Integration', () => {
let app;
beforeAll(async () => {
app = await setupTestServer();
});
afterAll(async () => {
await cleanupTestServer();
});
describe('Public Host', () => {
test('POST /api/upload should succeed', async () => {
const response = await request(app)
.post('/api/upload')
.set('X-Forwarded-Host', 'public.example.com')
.attach('images', Buffer.from('fake'), 'test.jpg');
expect(response.status).not.toBe(403);
});
test('GET /api/admin/deletion-log should be blocked', async () => {
const response = await request(app)
.get('/api/admin/deletion-log')
.set('X-Forwarded-Host', 'public.example.com');
expect(response.status).toBe(403);
});
test('GET /api/groups should be blocked', async () => {
const response = await request(app)
.get('/api/groups')
.set('X-Forwarded-Host', 'public.example.com');
expect(response.status).toBe(403);
});
});
describe('Internal Host', () => {
test('GET /api/admin/deletion-log should succeed', async () => {
const response = await request(app)
.get('/api/admin/deletion-log')
.set('X-Forwarded-Host', 'internal.example.com');
expect(response.status).not.toBe(403);
});
});
});
7.3 Frontend Tests
Neue Datei: frontend/src/Utils/__tests__/hostDetection.test.js
import { getHostConfig, isPublicHost, isInternalHost } from '../hostDetection';
describe('Host Detection', () => {
beforeEach(() => {
delete window._env_;
delete window.location;
});
test('detects public host correctly', () => {
window._env_ = {
PUBLIC_HOST: 'public.example.com',
INTERNAL_HOST: 'internal.example.com'
};
window.location = { hostname: 'public.example.com' };
const config = getHostConfig();
expect(config.isPublic).toBe(true);
expect(config.isInternal).toBe(false);
expect(config.canAccessAdmin).toBe(false);
});
test('detects internal host correctly', () => {
window._env_ = {
PUBLIC_HOST: 'public.example.com',
INTERNAL_HOST: 'internal.example.com'
};
window.location = { hostname: 'internal.example.com' };
const config = getHostConfig();
expect(config.isPublic).toBe(false);
expect(config.isInternal).toBe(true);
expect(config.canAccessAdmin).toBe(true);
});
test('localhost defaults to internal', () => {
window._env_ = {
PUBLIC_HOST: 'public.example.com',
INTERNAL_HOST: 'internal.example.com'
};
window.location = { hostname: 'localhost' };
const config = getHostConfig();
expect(config.isInternal).toBe(true);
});
});
7.4 E2E Test Checklist (Manuell)
- Upload auf
deinprojekt.hobbyhimmel.defunktioniert - Management-Portal (
/manage/:uuid) auf public Host funktioniert /slideshowauf public Host zeigt 404 / Not Found/groupsauf public Host zeigt 404 / Not Found/moderationauf public Host zeigt 404 / Not Found- Admin Login auf public Host blockiert (403)
- Alle Features auf
deinprojekt.lan.hobbyhimmel.defunktionieren - Navigation auf internal Host zeigt alle Menüpunkte
- Navigation auf public Host zeigt nur Upload-relevante Items
- Rate Limiting: 21. Upload/Stunde auf public Host wird blockiert
- DevTools Network: Internal-only JS-Bundles werden auf public nicht geladen
8. Sicherheits-Review
8.1 Threat Model
| Bedrohung | Mitigation | Status |
|---|---|---|
| Direkter API-Zugriff auf Admin-Endpoints von extern | Backend Middleware blockiert basierend auf Host | ✅ Implementiert |
| Frontend-Code-Analyse zeigt interne Features | Code Splitting verhindert Laden von internal Bundles | ✅ Implementiert |
| Rate Limiting Bypass | IP-basiertes Limiting + nginx-proxy-manager Logs | ✅ Implementiert |
| UUID-Token Leak | Management-Tokens sind per Design shareable; Rate Limit 10 req/h | ⚠️ Akzeptiertes Risiko |
| MITM-Angriffe | SSL/TLS auf beiden Hosts (Let's Encrypt) | ✅ Bestehend |
| CSRF auf Upload | CSRF-Protection in Middleware | ✅ Bestehend |
8.2 Security Checklist
ENABLE_HOST_RESTRICTION=truein ProductionTRUST_PROXY_HOPS=1korrekt gesetzt (nginx-proxy-manager)- SSL Zertifikate für beide Hosts gültig
- Rate Limits getestet (20 uploads/h)
- Admin-Endpoints per
curlvon extern getestet (403 expected) - Audit Logs enthalten
source_hostundsource_type - nginx-proxy-manager Block Common Exploits enabled
- HSTS enabled auf beiden Hosts
- Firewall: Port 5000 (Backend) nicht direkt von extern erreichbar
9. Dokumentation Updates
9.1 README.md Ergänzungen
Abschnitt: "Deployment - Production"
## Host-basierte Zugriffskontrolle
Die App unterstützt unterschiedliche Features abhängig von der Subdomain:
### Public Host (`deinprojekt.hobbyhimmel.de`)
- **Verfügbar**: Upload, Management-Portal (UUID-basiert)
- **Nicht verfügbar**: Admin, Moderation, Slideshow, Groups
- **Rate Limit**: 20 Uploads pro Stunde pro IP
### Internal Host (`deinprojekt.lan.hobbyhimmel.de`)
- **Verfügbar**: Alle Features (Full App)
- **Zugriff**: Nur über Intranet / VPN
### Konfiguration
Environment Variablen in `docker-compose.yml`:
```yaml
environment:
- PUBLIC_HOST=deinprojekt.hobbyhimmel.de
- INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de
- ENABLE_HOST_RESTRICTION=true
- PUBLIC_UPLOAD_RATE_LIMIT=20
nginx-proxy-manager Setup
- Erstelle zwei Proxy Hosts (public + internal)
- Beide leiten auf denselben Container (Port 80)
- SSL/TLS für beide Hosts aktivieren
X-Forwarded-HostHeader wird automatisch gesetzt
### 9.2 README.dev.md Ergänzungen
**Abschnitt**: "Development - Testing Host Restrictions"
```markdown
## Host Restriction Testing
### Dev Setup
In Development ist Host Restriction standardmäßig deaktiviert:
- `ENABLE_HOST_RESTRICTION=false` in `docker/dev/docker-compose.yml`
### Testing mit /etc/hosts
Füge lokale Host-Einträge hinzu:
```bash
sudo nano /etc/hosts
127.0.0.1 deinprojekt.hobbyhimmel.de
127.0.0.1 deinprojekt.lan.hobbyhimmel.de
Dann mit aktivierter Restriction testen:
ENABLE_HOST_RESTRICTION=true npm start
Browser Testing
- Public:
http://deinprojekt.hobbyhimmel.de:3000 - Internal:
http://deinprojekt.lan.hobbyhimmel.de:3000
### 9.3 CHANGELOG.md Eintrag
```markdown
## [Unreleased]
### Added
- **Host-basierte Zugriffskontrolle**: Unterschiedliche Features für public vs. internal Subdomain
- Public Host: Nur Upload + Management-Portal
- Internal Host: Vollständige App-Features
- Backend Middleware blockiert geschützte APIs für public Host
- Frontend Code Splitting: Internal Features werden auf public nicht geladen
- Rate Limiting: 20 Uploads/Stunde/IP für public Host
- Environment Variablen: `PUBLIC_HOST`, `INTERNAL_HOST`, `ENABLE_HOST_RESTRICTION`
### Changed
- Frontend: React Lazy Loading für internal-only Pages
- Backend: `hostGate` Middleware in Middleware-Stack integriert
- Audit Logs: `source_host` und `source_type` Felder hinzugefügt
- docker-compose: Environment Variablen für Host-Konfiguration
### Security
- Defense in Depth: Serverseitige API-Blockierung + Frontend Code Splitting
- Strengere Rate Limits für public Uploads
- Audit Logging für Public vs. Internal Requests
10. Rollout Plan
Phase 1: Development & Testing (Woche 1)
- Branch erstellen:
feature/public-internal-hosts - Backend Middleware implementieren (
hostGate.js) - Backend Tests schreiben und ausführen
- Frontend Utils implementieren (
hostDetection.js) - Frontend App.js mit Code Splitting anpassen
- Frontend Tests schreiben
- Dev-Setup testen (localhost mit /etc/hosts)
Phase 2: Staging Deployment (Woche 2)
- docker-compose.yml finalisieren
- env-config.js Injection testen
- nginx-proxy-manager Konfiguration vorbereiten
- DNS-Einträge für Staging erstellen
- SSL-Zertifikate für Staging-Hosts beantragen
- Staging Deployment durchführen
- E2E Tests auf Staging durchführen
Phase 3: Production Rollout (Woche 3)
- Security Review durchführen
- Production DNS-Einträge erstellen
- Production SSL-Zertifikate beantragen
- Production nginx-proxy-manager Hosts konfigurieren
- Production Deployment durchführen
- Monitoring & Logs prüfen (erste 24h)
- Rate Limiting Metriken auswerten
Phase 4: Dokumentation & Cleanup (Woche 4)
- README.md & README.dev.md finalisieren
- CHANGELOG.md aktualisieren
- Feature Request als "Done" markieren
- Branch mergen (nach Review)
- Deployment-Dokumentation für Wartung erstellen
11. Known Limitations & Future Improvements
Limitations
-
UUID-Token Permanence: Management-Tokens sind permanent gültig (bis Gruppe gelöscht)
- Risiko: Geleakte URLs bleiben gültig
- Mitigation: Rate Limiting (10 req/h), Audit Logging
-
Code Splitting nicht absolut: Mit genug Aufwand könnte jemand internal Code aus Bundle extrahieren
- Mitigation: Serverseitige API-Blockierung ist primäre Defense
-
Single Container: Frontend und Backend teilen sich Netzwerk-Namespace
- Akzeptiert: Für kleine Deployments ausreichend sicher
Future Improvements (Optional)
- Captcha für Public Uploads: reCAPTCHA v3 Integration für Abuse-Protection
- JWT-Tokens für Management: TTL-basierte Tokens statt permanenter UUIDs
- Separate Backend für Public: Microservices-Architektur (upload-only backend)
- CDN für Previews: Presigned URLs mit kurzen TTLs
- IP Whitelist für Admin: Zusätzliche IP-basierte Restriction für Admin-APIs
- WAF Integration: Web Application Firewall vor nginx-proxy-manager
12. Implementation Checklist
Backend
backend/src/middlewares/hostGate.jserstellenbackend/src/middlewares/index.jsanpassen (hostGate integrieren)backend/src/middlewares/rateLimiter.jsanpassen (publicUploadLimiter)backend/src/routes/upload.jsanpassen (publicUploadLimiter verwenden)backend/src/middlewares/auditLog.jsanpassen (source_host, source_type)backend/tests/unit/middlewares/hostGate.test.jserstellenbackend/tests/api/hostRestriction.test.jserstellen- Tests ausführen:
npm test
Frontend
frontend/src/Utils/hostDetection.jserstellenfrontend/src/App.jsanpassen (Code Splitting, ProtectedRoute)frontend/src/Components/Pages/404Page.jsanpassenfrontend/public/env-config.jserstellenfrontend/env.shanpassen (PUBLIC_HOST, INTERNAL_HOST)frontend/src/Utils/__tests__/hostDetection.test.jserstellen- Tests ausführen:
npm test
Docker & Config
docker/prod/docker-compose.ymlanpassen (Environment Variables)docker/dev/docker-compose.ymlanpassen (ENABLE_HOST_RESTRICTION=false)docker/prod/frontend/Dockerfileprüfen (env.sh Ausführung)
nginx-proxy-manager
- Public Host erstellen (
deinprojekt.hobbyhimmel.de) - Internal Host erstellen (
deinprojekt.lan.hobbyhimmel.de) - SSL-Zertifikate konfigurieren
- Advanced Settings testen (optional Route-Blocking)
Dokumentation
README.mdergänzen (Host-basierte Zugriffskontrolle)README.dev.mdergänzen (Testing Host Restrictions)CHANGELOG.mdaktualisierenFEATURE_REQUEST-FrontendPublic.mdals "Done" markieren
Testing
- Unit Tests (Backend & Frontend)
- Integration Tests (Backend API)
- E2E Tests (Manuell, siehe 7.4)
- Security Review (siehe 8.2)
13. Kontakt & Support
Bei Fragen oder Problemen während der Implementierung:
- GitHub Issues im Repository erstellen
- Feature Branch:
feature/public-internal-hosts - Reviewer: [Admin/Maintainer Name]
Status: Ready for Implementation
Geschätzte Implementierungszeit: 2-3 Wochen
Risiko: Medium (neue Middleware, Testing erforderlich)
Priorität: High (Sicherheitsfeature)