New instance switch readTrail.enabled, default OFF: read logging is employee monitoring in a works council's eyes — an ordinary instance must not surveil reads. Off means no event is written ANYWHERE (no row, no stdout line, verified by test); the api announces the switch position once per boot, so an eventless trail is never ambiguous — a gap reads as "was off", never "was lost". The written purpose limitation ships as section 7 of the VS-NfD security documentation (#228): what is recorded (no content, no titles, no IPs, no fingerprinting), why (evidence for reads of marked content only — variant A is the technical anchor of the promise), who may read it (Site Admin, API-only), for how long (readTrail.retentionDays, audited pruning), and what it may NOT be used for (no performance or behaviour monitoring). The hardening guide's reference configuration turns the trail on (reference value true) and points to that text; the existing trail suites now enable the switch explicitly. Refs #225. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
324 lines
22 KiB
Markdown
324 lines
22 KiB
Markdown
# Sicherheitsdokumentation (VS-NfD, Issue #228)
|
||
|
||
Zweck: das Dokument, das ein Prüfer zuerst liest — Architektur,
|
||
Datenflüsse, Netzplan, Vertrauensgrenzen und die vollständige Liste
|
||
aller Inhaltskopien. Es konsolidiert bestehende Referenzdokumente,
|
||
erfindet nichts neu und ist **auf die Ziffer genau** an
|
||
`deploy/compose/docker-compose.yml` ausgerichtet (die Compose-Datei ist
|
||
die maßgebliche Dienst- und Portliste; weicht dieses Dokument ab, ist
|
||
das ein Fehler in diesem Dokument).
|
||
|
||
Einordnung: Dorfteich erbringt **keine Sicherheitsgrundfunktion**
|
||
(§52 VSA, ADR 0019, Abgrenzungserklärung
|
||
`40-abgrenzungserklaerung.md`). Diese Dokumentation beschreibt, was die
|
||
Anwendung tut — Verschlüsselung, Netztrennung, Datenträgerschutz und
|
||
Beweissicherung auf Plattformebene fallen dem Betreiber zu.
|
||
|
||
Quell-Dokumente (englisch, maßgeblich für Details):
|
||
`docs/architecture/security.md`, `deployment.md`, `data-model.md`,
|
||
`permissions.md`, `realtime-collaboration.md`,
|
||
`plugin-architecture.md`, `operations.md`, `audit-events.md`;
|
||
Deploy-Sicht: `deploy/stages.md`, `deploy/compose/docker-compose.yml`.
|
||
|
||
Pflegeregel: Änderungen an Diensten, Ports, Datenflüssen oder
|
||
Speicherorten werden **im selben PR** hier nachgezogen (dieselbe Regel
|
||
wie beim Härtungsleitfaden `50-haertungsleitfaden.md`).
|
||
|
||
---
|
||
|
||
## 1 Komponenten
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph client [Endgerät]
|
||
B[Browser SPA]
|
||
end
|
||
subgraph host [Betreiber-Host / Reverse-Proxy-Zone]
|
||
RP[Reverse Proxy TLS]
|
||
end
|
||
subgraph frontend [Docker-Netz frontend]
|
||
W[web nginx statisch]
|
||
A[api NestJS]
|
||
C[collab Hocuspocus]
|
||
end
|
||
subgraph internal [Docker-Netz internal]
|
||
DB[(PostgreSQL 17.5)]
|
||
P[pandoc 3.6]
|
||
G[gotenberg 8]
|
||
BK[backup Sidecar]
|
||
end
|
||
SMTP[(SMTP-Relay extern)]
|
||
MIR[(Backup-Spiegel rsync/WebDAV, Allowlist)]
|
||
|
||
B -->|HTTPS 443| RP
|
||
RP -->|/| W
|
||
RP -->|/api| A
|
||
RP -->|/collab WebSocket| C
|
||
A --> DB
|
||
C --> DB
|
||
A -->|HTTP pandoc:3030| P
|
||
A -->|HTTP gotenberg:3000| G
|
||
A -->|Mail| SMTP
|
||
BK --> DB
|
||
BK -.->|nur bei Konfiguration + Allowlist| MIR
|
||
BK -->|Fehler-Mail| SMTP
|
||
```
|
||
|
||
| Dienst | Image (gepinnt, #203/#236) | Zweck | Privilegien |
|
||
| ---------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||
| `web` | eigenes Image (nginx-unprivileged 1.27, uid 101) | statische SPA-Auslieferung | kein Volume, nur Netz `frontend`, kein DB-Zugang |
|
||
| `api` | eigenes Image (Node 22.15.1-alpine, User `node`) | REST-API, Permissions, Scheduler-Jobs, Import/Export-Orchestrierung, Audit | Volumes `uploads`, `plugins`, `secrets`, `backups` (ro); Netze `frontend`+`internal`; DB-Vollzugriff |
|
||
| `collab` | eigenes Image (Node, User `node`) | Echtzeit-Editing (Yjs/Hocuspocus), Persistenz der Dokumente | Netze `frontend`+`internal`; DB-Vollzugriff; keine Datei-Volumes |
|
||
| `db` | `postgres:17.5-alpine@sha256:…` | einziger Datenbestand (außer Uploads) | nur Netz `internal`; Volume `db-data` |
|
||
| `pandoc` | `pandoc/core:3.6@sha256:…` | Dokumentkonvertierung (Import/Export DOCX/ODT) | nur `internal`; zustandslos, kein Volume, kein DB-Zugang |
|
||
| `gotenberg` | `gotenberg/gotenberg:8@sha256:…` | HTML→PDF-Rendering (Chromium) | nur `internal`; zustandslos, kein Volume, kein DB-Zugang |
|
||
| `backup` | eigenes Image | nächtlicher `pg_dump -Fc` + Volume-Tar, Retention, optionaler Spiegel | nur `internal`; Volumes `uploads`, `plugins`, `secrets` (ro), `backups` (rw); DB-Zugang |
|
||
| `caddy` (Compose-Profil, optional) | `caddy:2.10-alpine@sha256:…` | TLS-Terminierung, wenn kein Host-Proxy existiert | Ports 80/443 nach außen; Netz `frontend` |
|
||
|
||
Alle eigenen Images laufen als non-root (`USER node`); die Stages auf
|
||
ONE nutzen einen Host-Reverse-Proxy (Caddy des Hosts), das
|
||
`caddy`-Profil bleibt dort inaktiv.
|
||
|
||
## 2 Netzplan (Ports und Protokolle)
|
||
|
||
Maßgeblich: `deploy/compose/docker-compose.yml`. Alle
|
||
Anwendungs-Portbindungen sind **auf 127.0.0.1 beschränkt** — von außen
|
||
erreichbar ist ausschließlich der Reverse Proxy.
|
||
|
||
| Von | Nach | Port/Protokoll | Exposition |
|
||
| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
||
| Client | Reverse Proxy | 443/TCP HTTPS (80 nur Redirect) | **extern** |
|
||
| Reverse Proxy | `web` | `127.0.0.1:${WEB_PORT:-8100}` → Container 8080, HTTP | Host-lokal |
|
||
| Reverse Proxy | `api` | `127.0.0.1:${API_PORT:-8101}` → Container 3000, HTTP | Host-lokal |
|
||
| Reverse Proxy | `collab` | `127.0.0.1:${COLLAB_PORT:-8102}` → Container 3000, HTTP + WebSocket-Upgrade | Host-lokal |
|
||
| `api`/`collab`/`backup` | `db` | 5432/TCP (nur Docker-Netz `internal`) | intern |
|
||
| `api` | `pandoc` | `http://pandoc:3030` (nur `internal`) | intern |
|
||
| `api` | `gotenberg` | `http://gotenberg:3000` (nur `internal`) | intern |
|
||
| `api`, `backup` | SMTP-Relay | vom Betreiber konfiguriert (`SMTP_*`) | **ausgehend extern** |
|
||
| `backup` | Spiegel-Ziel | rsync über SSH (Port konfigurierbar) bzw. WebDAV/HTTPS — **nur** an Hosts der Deploy-Allowlist `BACKUP_ALLOWED_TARGETS` (ADR 0026, #192); leere Allowlist = alle Fernziele hart aus | **ausgehend extern, allowlist-beschränkt** |
|
||
| optional `caddy` | Client | 80/443 | extern (nur wenn Profil aktiv) |
|
||
|
||
Es gibt **keine eingehenden** Verbindungen außer über den Reverse
|
||
Proxy. `db`, `pandoc`, `gotenberg`, `backup` haben keinerlei
|
||
Portbindung an den Host.
|
||
|
||
## 3 Datenflüsse
|
||
|
||
### 3.1 Authentifizierung
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant B as Browser
|
||
participant A as api
|
||
participant DB as PostgreSQL
|
||
B->>A: POST /api/v1/auth/login (Origin-Header Pflicht, fail-closed CSRF #189)
|
||
A->>DB: user_identities (Argon2id-Hash prüfen), rate_limits (10/min/IP)
|
||
A-->>B: Set-Cookie dt_session (HttpOnly, Secure, SameSite=Lax)
|
||
Note over A,DB: Session-Row: gehashte Id, expiresAt (SESSION_ABSOLUTE_HOURS),<br/>Idle-Grenze (SESSION_IDLE_HOURS, #190); Audit auth.login_*
|
||
```
|
||
|
||
Externe Authentisierung (OIDC/Proxy-Header) ist geplant (M27,
|
||
#214–#217) und ändert diesen Fluss; bis dahin ist lokale
|
||
Passwort-Authentifizierung der einzige Weg.
|
||
|
||
### 3.2 Editieren (Echtzeit)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant B as Browser
|
||
participant A as api
|
||
participant C as collab
|
||
participant DB as PostgreSQL
|
||
B->>A: GET /pages/:id/collab-token (Guard: Read-Recht)
|
||
A-->>B: Kurzlebiges Token (60 s TTL, HKDF-Subkey aus COLLAB_TOKEN_SECRET, #188), mode rw/ro
|
||
B->>C: WebSocket /collab + Token
|
||
C->>C: Token-Verifikation (shared token-crypto), rw nur bei Write-Grant
|
||
B-->>C: Yjs-Updates (CRDT)
|
||
C->>DB: debounced Persist: pages.ydoc_state + page_updates-Log,<br/>abgeleiteter page_content_cache (plain/markdown/html/outline/tsvector)
|
||
A-->>C: Rechteänderungen via Postgres LISTEN/NOTIFY (Kanal-Konstanten shared) → close(4205)
|
||
```
|
||
|
||
### 3.3 Export (PDF/DOCX/ODT/ZIP)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant B as Browser
|
||
participant A as api
|
||
participant P as pandoc
|
||
participant G as gotenberg
|
||
B->>A: POST /pages/:id/export {format}
|
||
A->>A: conversion_jobs-Row (input = aufbereitetes Markdown/HTML,<br/>bei eingestufter Seite Option {marking}, #208/#209)
|
||
A->>G: html → pdf (Kennzeichnung im Header/Footer-Template je Seite)
|
||
A->>P: gfm → docx/odt (reference-doc mit Kennzeichnung, in-Request-Datei)
|
||
A-->>B: Poll GET /jobs/:id → Download /jobs/:id/result
|
||
Note over A: Payloads werden nach conversion.payloadRetentionDays genullt (#233)
|
||
```
|
||
|
||
Der Pond-ZIP-Export streamt direkt aus `page_content_cache` +
|
||
`uploads`-Volume (permissionsgefiltert) und enthält seit M26
|
||
Frontmatter/Aufdruck, `manifest.json` und Begleitdateien für
|
||
eingestufte Inhalte (#210/#212).
|
||
|
||
### 3.4 Backup
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant BK as backup-Sidecar
|
||
participant DB as PostgreSQL
|
||
participant V as Volumes uploads/plugins
|
||
participant M as Spiegel (optional)
|
||
BK->>DB: nächtlich pg_dump -Fc
|
||
BK->>V: tar der Volumes (lesend)
|
||
BK->>BK: Restore-Set aufs backups-Volume, Retention (BACKUP_RETENTION_DAYS)
|
||
BK-->>M: rsync/WebDAV NUR an BACKUP_ALLOWED_TARGETS (ADR 0026); leer = aus
|
||
BK-->>BK: status.json; Fehler-Mail direkt via SMTP
|
||
```
|
||
|
||
Backups sind **unverschlüsselt by design** (ADR 0015/0019) —
|
||
Datenträger- und Transportschutz ist Plattformsache
|
||
(Abgrenzungserklärung §2).
|
||
|
||
### 3.5 Lesekanäle
|
||
|
||
Jeder Kanal, über den Seiteninhalt die Anwendung verlässt (zugleich die
|
||
Instrumentierungsliste für den Lesetrail, #222). Der Lesetrail
|
||
protokolliert je (Sitzung, Seite, Kanal) **ein Ereignis pro
|
||
Dedup-Fenster** (`readTrail.dedupWindowMinutes`, Default 5 Minuten,
|
||
#223); jede Zeile trägt die Fensterlänge. Beweiswert damit: „mindestens
|
||
ein Zugriff in diesem Fenster" — **nicht** eine Zugriffszählung, und
|
||
beim Kanal Collab „Live-Verbindung bestand während des Fensters", nicht
|
||
einzelne Sync-Frames. Anonyme Leser teilen sich den Marker `anon`
|
||
(keine Fingerprinting-Unterscheidung, Zweckbindung #225):
|
||
|
||
| Kanal | Pfad | Rechteprüfung | Kennzeichnung (M26) |
|
||
| -------------------------- | -------------------------------- | -------------------------------------- | ----------------------------------- |
|
||
| SPA-Seitenansicht | `GET /api/v1/pages/:id` u. a. | Guard (shared Resolver) | Banner oben+unten (#206) |
|
||
| Public-/No-JS-Ansicht | `GET /api/v1/public/:pond/:page` | `public`-Grant, sonst 404 | Banner oben+unten (#211) |
|
||
| Public REST API | `/api/public/v1/**` | `api.enabled` + Pond-Opt-in + PAT | `classification`-Feld (#211) |
|
||
| MCP-Endpoint | `/api/mcp` | `mcp.enabled` + Pond-Opt-in + PAT | wie Public API (gleiches Modell) |
|
||
| Atom-Feeds | `…/feed.xml` | `feeds.enabled`; Grant bzw. Feed-Token | `<category>`-Element (#211) |
|
||
| Suche | `GET /api/v1/search` | per-Treffer-Resolution | Level je Treffer (#211) |
|
||
| Attachment-Download | `GET /api/v1/media/:fileId` | Guard über Seite/Pond | Dateinamens-Präfix `VS-NfD_` (#212) |
|
||
| Export (PDF/Office/ZIP/MD) | s. 3.3 | Guard; ZIP permissionsgefiltert | je Kanal (#207–#210) |
|
||
| Collab-WebSocket | `/collab` | Token aus 3.2 | Inhalt = Editor-Ansicht (#206) |
|
||
|
||
## 4 Vertrauensgrenzen
|
||
|
||
1. **Client ↔ Instanz** (Internet/Behördennetz): TLS am Reverse Proxy
|
||
terminiert (Betreiber-Zone). Die Anwendung setzt die
|
||
Security-Header selbst (eigene Middleware, #197: CSP `script-src
|
||
'self'`, `X-Frame-Options: SAMEORIGIN`, restriktives CORS);
|
||
Cookie-Mutationen sind Origin-pflichtig (fail-closed, #189).
|
||
2. **Reverse Proxy ↔ Anwendungscontainer**: nur 127.0.0.1-Bindungen;
|
||
der Proxy ist die einzige Eintrittsstelle. Der geplante
|
||
Proxy-Header-/mTLS-Authentisierungspfad (#215) verschiebt die
|
||
Authentisierungs-Vertrauensgrenze an genau diese Stelle — bis dahin
|
||
trägt der Proxy nur Transport.
|
||
3. **Anwendungs- ↔ Datenzone**: `db`, `pandoc`, `gotenberg`, `backup`
|
||
sind nur im Docker-Netz `internal` erreichbar; `web` hat keinerlei
|
||
Zugang dorthin.
|
||
4. **Plugin-Sandbox** (ADR 0008): Plugin-Code läuft ausschließlich im
|
||
sandboxed iframe (eigene Origin-lose Umgebung, Message-Protokoll,
|
||
deklarierte Capabilities); serverseitig wird beim Install ein
|
||
Static-Gate erzwungen. Instanzweiter Kill-Switch `plugins.enabled`
|
||
(#200); Hash-Pinning ist bewusst verschoben (#232,
|
||
Restrisikoliste). Der Sandbox-Escape-Regressionstest (bösartiges
|
||
Fixture-Plugin) läuft in CI.
|
||
5. **Ausgehende Kanäle**: SMTP (Betreiber-Relay) und Backup-Spiegel
|
||
(Allowlist, ADR 0026) sind die einzigen initiierten
|
||
Außenverbindungen. Es gibt keine Telemetrie, keine Update-Pings,
|
||
keine externen Font-/CDN-Ladungen (ADR 0016: Fonts self-hosted).
|
||
6. **Betreiber-Plattform**: Host, Docker-Daemon, Volumes, Netzwerk und
|
||
Backups liegen außerhalb der Anwendungsverantwortung
|
||
(Abgrenzungserklärung).
|
||
|
||
## 5 Vollständige Liste der Inhaltskopien
|
||
|
||
Grundlage des Löschkonzepts im Betriebshandbuch
|
||
(`70-betriebshandbuch.md` §5) — Löschen ist nur vollständig, wenn es
|
||
jede dieser Kopien erreicht oder ihren Verbleib begründet.
|
||
|
||
**In der Datenbank (Volume `db-data`):**
|
||
|
||
| Ort | Inhalt | Lebenszyklus |
|
||
| ------------------------------ | ------------------------------------------------- | ----------------------------------------------------- |
|
||
| `pages.ydoc_state` | aktuelles Dokument (Yjs-Binärzustand) | bis Purge der Seite |
|
||
| `page_updates` | inkrementelles Update-Log | Compaction; Purge löscht |
|
||
| `page_content_cache` | Klartext, Markdown, HTML, Outline, **Suchvektor** | bei jedem Persist ersetzt; Purge löscht |
|
||
| `page_versions` | eigenständige Snapshots (Historie) | bis Purge der Seite |
|
||
| `comments` | Kommentartexte | Purge der Seite kaskadiert |
|
||
| `conversion_jobs.input/result` | Export-/Import-Payloads (ganze Dokumente) | genullt nach `conversion.payloadRetentionDays` (#233) |
|
||
| `mail_outbox` | Mail-Bodies (Digest-Titel!) | gelöscht nach `mail.outboxRetentionDays` (#234) |
|
||
| `page_links.target_slug` | Slug-Text auch nach Ziel-Purge | bewusst behalten (#235, Restrisiko I-24) |
|
||
| `audit_log` | Metadaten (nie Inhalt) | `audit.retentionDays` (#196) |
|
||
| `attachments` (Row) | Metadaten + SHA-256 | Purge der Seite/des Ponds |
|
||
|
||
**Auf Volumes:**
|
||
|
||
| Ort | Inhalt | Lebenszyklus |
|
||
| --------- | -------------------------------------------- | --------------------------------------------------------------- |
|
||
| `uploads` | Attachment-Bytes | Purge löscht Datei + Quota; Orphan-Sweep (#194) räumt Verwaiste |
|
||
| `backups` | nächtliche Restore-Sets (Dump + Volume-Tars) | `BACKUP_RETENTION_DAYS` |
|
||
| `plugins` | Plugin-Pakete (kein Seiteninhalt) | Uninstall |
|
||
| `secrets` | Secret-Store (SMTP etc., kein Seiteninhalt) | Betreiber |
|
||
|
||
**Außerhalb der Instanz (nicht von Anwendungs-Löschung erreichbar —
|
||
Löschkonzept muss sie benennen):**
|
||
|
||
| Ort | Inhalt | Kontrolle |
|
||
| ----------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||
| Backup-Spiegel (BASEL/WebDAV) | vollständige Restore-Sets | Betreiber; Retention auf Zielsystem |
|
||
| Export-Artefakte | PDF/DOCX/ODT/ZIP beim Nutzer | organisatorisch (VS-Handhabung, Kennzeichnung M26) |
|
||
| Browser-IndexedDB | Offline-Kopie zuletzt geöffneter Seiten | Endgeräteschutz (Restrisiko I-25) |
|
||
| Feed-Reader / API-Konsumenten | abonnierte Inhalte | organisatorisch; Referenzkonfiguration schaltet Feeds/API ab |
|
||
| Mails beim Empfänger | Digest-/Benachrichtigungstexte (Titel) | Restrisiko I-23; Referenzkonfiguration ohne SMTP |
|
||
| Container-stdout-Logs | Metadaten inkl. Audit-Zeilen (nie Seiteninhalt, security.md §Logging) | Docker json-file mit Rotation; Collector des Betreibers |
|
||
|
||
## 6 Kryptografie-Inventar (Bestand, kein Anspruch)
|
||
|
||
Zur Einordnung (vollständige Liste: Abgrenzungserklärung §3): Argon2id
|
||
für Passwort-Hashes, HMAC-signierte Kurzzeit-Tokens über `jose` mit
|
||
HKDF-Zweckableitung (#188), SHA-256-Integritätshashes für Attachments
|
||
(#199, fail-closed beim Download). Keine Inhalts- oder
|
||
Backup-Verschlüsselung, kein eigenes Schlüsselmanagement — bewusst
|
||
(§52 VSA).
|
||
|
||
## 7 Lesetrail: Zweckbindung (Issue #225, ADR 0023)
|
||
|
||
Der Lesetrail ist abschaltbar (`readTrail.enabled`, **Default: aus**);
|
||
die Referenzkonfiguration (Härtungsleitfaden §1.1) schaltet ihn ein.
|
||
Aus heißt: es wird **nirgends** ein Ereignis geschrieben — keine
|
||
Tabellenzeile, keine Logzeile; die api meldet die Schalterstellung
|
||
einmal beim Start, damit ein ereignisloser Trail nie zweideutig ist.
|
||
Diese Zweckbindung ist Teil der Betriebsdokumentation und gegenüber
|
||
der Personalvertretung offenzulegen.
|
||
|
||
**Was aufgezeichnet wird:** je (Sitzung, Seite, Kanal) und Dedup-Fenster
|
||
(§3.5) ein Ereignis mit Zeitstempel, Konto-Id (oder dem Marker `anon`),
|
||
Sitzungsschlüssel, Seiten- und Teich-Id, Kanal und der Einstufung zum
|
||
Lesezeitpunkt. **Kein** Seiteninhalt, **keine** Titel, **keine**
|
||
IP-Adressen, **kein** Fingerprinting anonymer Leser.
|
||
|
||
**Warum:** ausschließlich Beweissicherung für Lesezugriffe auf **als
|
||
VS-NfD gekennzeichnete** Inhalte (§52-konforme Nachvollziehbarkeit:
|
||
„welche eingestufte Seite wurde wann über welchen Kanal gelesen") —
|
||
Plattform-Logs kennen URLs, nicht Einstufungen. Zugriffe auf nicht
|
||
eingestufte Inhalte werden **nie** erfasst (Variante A, ADR 0023).
|
||
|
||
**Wer lesen darf:** ausschließlich Site-Admins über
|
||
`GET /admin/system/read-events` (kein UI-Panel — Prüfwerkzeug, kein
|
||
Alltagsbildschirm). Organisatorische Beschränkung auf den
|
||
Sicherheitsbeauftragten: Betreibersache (Betriebshandbuch §6
|
||
Rollentrennung).
|
||
|
||
**Wie lange:** `readTrail.retentionDays` (Default 365 Tage); Löschläufe
|
||
sind selbst auditiert (`read_trail.pruned`), sodass jede Lücke erklärbar
|
||
ist.
|
||
|
||
**Wofür NICHT:** keine Leistungs- oder Verhaltenskontrolle der
|
||
Beschäftigten, keine Auswertung von Arbeitsmustern, keine
|
||
Anwesenheitskontrolle, keine Weitergabe außerhalb des
|
||
Sicherheitsvorfalls- bzw. Prüfkontexts. Die Beschränkung auf
|
||
eingestufte Inhalte ist die technische Absicherung dieser Zusage: was
|
||
nicht gekennzeichnet ist, erzeugt keine Spur.
|