docs: VS-NfD readiness planning (ist-aufnahme, plan, ADRs 0019-0026, issue drafts)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m42s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m47s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m38s
CI / Import/export fidelity gate (push) Successful in 56s
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m42s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m47s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m38s
CI / Import/export fidelity gate (push) Successful in 56s
Add docs/vs-nfd/: the analysis brief, the as-is assessment (42 findings, all verified against the code), the prioritized action plan rev. 2 with issue references written back to every checkbox, the two-stage issue/ADR brief, and the full reviewed draft used to create the forge state. Add eight proposed ADRs 0019-0026 covering the VS-NfD architecture decisions: no security base functions (par. 52 VSA anchor), HKDF token key separation, external authentication, page classification, read-access audit trail (variant A), reproducible offline deployment, plugin trust model, and backup target restriction. Forge state created alongside this commit: 11 labels, milestones M24-M31, issues #188-#236 (docs-only change, no code touched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
This commit is contained in:
parent
b5d2a436e0
commit
fd07f716f6
82
docs/architecture/adr/0019-no-security-base-functions.md
Normal file
82
docs/architecture/adr/0019-no-security-base-functions.md
Normal file
@ -0,0 +1,82 @@
|
||||
# ADR 0019: No security base functions in the application (§52 VSA)
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
Dorfteich is to be operable inside an IT environment of a German federal
|
||||
authority that is approved under the Verschlusssachenanweisung (VSA), for
|
||||
content classified VS-NfD. **No BSI certification of Dorfteich itself is
|
||||
sought.**
|
||||
|
||||
§51 VSA makes products that provide a _Sicherheitsgrundfunktion_ subject to
|
||||
certification. §52 VSA enumerates those base functions: encryption, media
|
||||
protection (Datenträgerschutz), network termination (Netzabschluss), and
|
||||
authentication. A product that implements one of them itself moves into the
|
||||
certification obligation — an outcome that would end this undertaking on
|
||||
cost grounds alone.
|
||||
|
||||
Today Dorfteich sits close to the right side of that line, partly by
|
||||
accident and partly by design: there is no content encryption, no backup
|
||||
encryption, no own MFA, and no cryptographic primitive of our own beyond
|
||||
signing short-lived collaboration tokens and hashing credentials. What is
|
||||
missing is the _decision_ — so that no future feature crosses the line
|
||||
because nobody had written down where it runs.
|
||||
|
||||
## Decision
|
||||
|
||||
**Dorfteich does not provide any security base function within the meaning
|
||||
of §52 VSA. Encryption, media protection, network termination and
|
||||
authentication belong to the operator's platform.**
|
||||
|
||||
Concretely:
|
||||
|
||||
1. **No encryption of content**, neither in the database nor on the file
|
||||
system. Confidentiality of stored data is provided by the platform
|
||||
(full-disk / volume encryption).
|
||||
2. **No backup encryption in the application.** Media protection is the
|
||||
platform's function; the application restricts _where_ backups may go
|
||||
(ADR 0026) and nothing more.
|
||||
3. **No own MFA, no own password policy engine.** Authentication is
|
||||
delegated to the operator's identity provider (ADR 0021). Local
|
||||
passwords remain available for non-VS deployments and are hard-switchable
|
||||
off.
|
||||
4. **No new cryptographic primitives.** Existing crypto is limited to
|
||||
credential hashing (Argon2id), token hashing (SHA-256) and signing
|
||||
short-lived tokens, and it uses vetted libraries rather than
|
||||
hand-written constructions (ADR 0020).
|
||||
5. **No TLS termination, no network segmentation** in the application.
|
||||
6. **No application-side separation of classification levels.** Levels are
|
||||
separated by operating one instance per level; the application only
|
||||
_marks_ content (ADR 0022).
|
||||
|
||||
The application's contribution to security is a different set of
|
||||
properties, and these it does own: a central, default-closed permission
|
||||
model; complete absence of outbound connections; verifiable marking of
|
||||
classified content in every output channel; and an audit trail.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Deliberate non-features must be argued as architecture, not apologised
|
||||
for as gaps. "No encryption in the code" is the correct division of
|
||||
labour under §52 VSA.
|
||||
- Every feature proposal is measured against this ADR. Any change that
|
||||
would make the application the bearer of a base function needs to amend
|
||||
this ADR first — which is the point of writing it down.
|
||||
- The operator carries obligations that must be handed over explicitly and
|
||||
in writing. This ADR is therefore the **draft of the delimitation
|
||||
statement** (Abgrenzungserklärung) that #226 turns into a
|
||||
reviewer-facing document; the two must not diverge.
|
||||
- Anything the platform cannot supply because it lacks application
|
||||
knowledge stays with us. Two cases exist today: marking of classified
|
||||
content (only the application knows the classification, ADR 0022) and
|
||||
integrity of the application's own payloads (#199).
|
||||
- Residual risks arising from delegation are listed in #231 rather than
|
||||
silently accepted.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#226 (delimitation statement), #227 (hardening guide), #228 (security
|
||||
documentation), #229 (operations manual), #230 (IT-Grundschutz mapping),
|
||||
#231 (residual-risk list).
|
||||
59
docs/architecture/adr/0020-token-crypto-key-separation.md
Normal file
59
docs/architecture/adr/0020-token-crypto-key-separation.md
Normal file
@ -0,0 +1,59 @@
|
||||
# ADR 0020: Token crypto — HKDF key separation and a vetted JWT library
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
`COLLAB_TOKEN_SECRET` currently signs two unrelated kinds of token:
|
||||
short-lived collaboration tokens (issue #34) and long-lived unsubscribe
|
||||
tokens in outgoing mail. A single secret across purposes means a
|
||||
compromise in one path is transferable to the other.
|
||||
|
||||
`packages/shared/src/token-crypto.ts` implements the compact HS256 JWT by
|
||||
hand on `node:crypto`. The reason is documented in the file and is a good
|
||||
one: the identical code has to run in the CommonJS api and the ESM collab
|
||||
server without module-interop or dependency-version drift. The
|
||||
implementation is careful — HS256 only, constant-time comparison before
|
||||
any untrusted field is read. It is nevertheless hand-written crypto in the
|
||||
trust boundary, which is a finding in any assessment regardless of its
|
||||
quality.
|
||||
|
||||
ADR 0019 states the application implements no security base function.
|
||||
Token signing is not one — but it is crypto we do perform, so it has to be
|
||||
minimal, purpose-bound and delegated to a vetted implementation.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Purpose-bound subkeys via HKDF.** The configured secret becomes a root
|
||||
key from which each purpose derives its own subkey (collaboration
|
||||
tokens, unsubscribe tokens, any future purpose). No code path signs with
|
||||
the root key.
|
||||
2. **`jose` replaces the homegrown JWT.** It is maintained, audited,
|
||||
works in both module systems, and is dependency-free — which matters for
|
||||
the supply-chain argument. HS256 stays the only accepted algorithm, as
|
||||
an explicit allowlist rather than an implicit default.
|
||||
3. **Purpose separation is structural, not textual.** The existing
|
||||
`PURPOSE` string prefix in `unsubscribe-token.ts` is superseded by key
|
||||
separation; a token signed for one purpose cannot verify under another
|
||||
because the key differs.
|
||||
4. **Long-lived tokens get a documented dual-verify window.** Unsubscribe
|
||||
links live in mail that has already been sent, so both derivations are
|
||||
accepted for a stated period with a stated expiry date. The window is a
|
||||
documented fact, not an accident.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The cross-runtime property that motivated the hand-written code must be
|
||||
proven by a test, not assumed — otherwise the reason for the original
|
||||
decision is lost silently.
|
||||
- One new runtime dependency. Accepted: `jose` has no transitive
|
||||
dependencies, so the supply-chain delta is one package.
|
||||
- Rotating the root key invalidates all derived subkeys at once, which is
|
||||
the desired behaviour and needs documenting in the operations manual.
|
||||
- The key hierarchy becomes part of the security documentation (#228) and
|
||||
the delimitation statement's crypto section (#226).
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#188.
|
||||
71
docs/architecture/adr/0021-external-authentication.md
Normal file
71
docs/architecture/adr/0021-external-authentication.md
Normal file
@ -0,0 +1,71 @@
|
||||
# ADR 0021: External authentication via OIDC; local passwords optional
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0007 established sessions and identities with OIDC in mind:
|
||||
`UserIdentity.provider` is documented as `"password"` today and
|
||||
`"oidc:<issuer>"` later, with `@@unique([provider, subject])` already in
|
||||
place. No OIDC code exists — the readiness is structural only.
|
||||
|
||||
Authentication is a security base function under §52 VSA (ADR 0019), so it
|
||||
belongs to the operator's platform. An authority environment additionally
|
||||
brings its own account lifecycle: joiners, movers and leavers are managed
|
||||
in the IdP, and a second account store inside the application would drift
|
||||
from it.
|
||||
|
||||
Some environments terminate authentication at the perimeter instead and
|
||||
expect the application to trust a header or a client certificate.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **OIDC Authorization Code with PKCE is the primary path**, configured by
|
||||
discovery, validated against JWKS. Keycloak is the reference IdP we
|
||||
verify against; nothing in the implementation is Keycloak-specific.
|
||||
2. **Identities use the existing slot**: `provider = "oidc:<issuer>"`,
|
||||
`subject` from the token. Linking an OIDC identity to an existing local
|
||||
user follows an explicit, documented rule — never silently by e-mail
|
||||
address, which would be an account-takeover path.
|
||||
3. **Local authentication is switchable off in full**, via
|
||||
`auth.local.enabled = false`. "In full" means every credential-issuing
|
||||
flow: password login, self-service signup, password reset,
|
||||
verification-as-login, and the token flows (PAT, feed tokens). A
|
||||
half-closed local path makes the operating concept untrue, which is
|
||||
worse than not closing it.
|
||||
4. **Proxy header and mTLS are a supported alternative path, off by
|
||||
default.** When enabled they require an allowlist of trusted peers; a
|
||||
request carrying the header from an untrusted peer is rejected and
|
||||
audited. The trust boundary is stated explicitly in the security
|
||||
documentation.
|
||||
5. **Claims map onto the existing permission model** declaratively, and
|
||||
mapped grants are written through the same service path as manual ones
|
||||
so the permission cache stays correct. The application gains no second
|
||||
authorization model.
|
||||
6. **No MFA, no password policy engine of our own** (ADR 0019). Both are
|
||||
the IdP's.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Bootstrapping needs a documented answer: the first-run wizard creates a
|
||||
local admin, so either it stays exempt with a stated compensating
|
||||
control, or setup itself runs against the IdP. The choice is recorded in
|
||||
#216.
|
||||
- Whether the switch is deploy-level or runtime matters: a runtime setting
|
||||
can be flipped back by a compromised Site-Admin. If it stays runtime,
|
||||
that residual risk goes into #231.
|
||||
- Existing password hashes remain in the database after the switch. Their
|
||||
deletion is out of scope and, being Argon2id, they are not a
|
||||
confidentiality problem — but the fact is documented.
|
||||
- Session handling is unchanged: OIDC produces a session through the same
|
||||
service, so there is exactly one session mechanism (see #190 for its
|
||||
bounds).
|
||||
- SAML and LDAP stay out. OIDC plus proxy/mTLS covers the environments we
|
||||
target; adding SAML would be a new decision.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#214 (OIDC + PKCE), #215 (proxy header / mTLS), #216
|
||||
(`auth.local.enabled`), #217 (claim mapping). Depends on #188 for the
|
||||
vetted JWT implementation.
|
||||
78
docs/architecture/adr/0022-page-classification.md
Normal file
78
docs/architecture/adr/0022-page-classification.md
Normal file
@ -0,0 +1,78 @@
|
||||
# ADR 0022: Classification as first-class page metadata
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
VS-NfD content must be marked, in every output that leaves the system.
|
||||
Dorfteich has no classification concept today: `model Page` carries title,
|
||||
slug, tree position and timestamps, and nothing else that could express a
|
||||
protection level.
|
||||
|
||||
The obvious shortcut is to reuse labels. Verification shows why that
|
||||
fails:
|
||||
|
||||
- `Label` is **pond-scoped** (`pondId`), so the same classification would
|
||||
be a different object in every pond, with no instance-wide meaning.
|
||||
- Labels are **user-editable** by any editor; a marking must not be
|
||||
removable as a matter of routine content work.
|
||||
- Labels do **not inherit** down the page tree, so a subpage of classified
|
||||
content would silently be unmarked.
|
||||
- Labels **never leave the application**: `export.service.ts` loads
|
||||
`labelIds` only to feed `permissions.filterPages`, and no export path
|
||||
writes them out. A carrier that does not reach the output channels cannot
|
||||
serve as a marking.
|
||||
|
||||
The second question is architectural: should the application separate
|
||||
classification _levels_? It must not (ADR 0019, and the plan's Phase 0
|
||||
guardrails). Separation is a platform property.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **A dedicated enum field on `Page`**, with an instance-wide default from
|
||||
`instance_settings`. Not labels, for the four reasons above.
|
||||
2. **Separation of levels happens outside the application: one instance per
|
||||
classification level.** The application marks; it does not isolate.
|
||||
This is the central operational decision of the whole undertaking and
|
||||
belongs here rather than in a manual, because it defines what the
|
||||
feature is _not_.
|
||||
3. **The application-side ACL is order, not a protection mechanism.**
|
||||
Permissions keep working as they do (central, default-closed,
|
||||
deny-wins), and the classification field does not change them. Anyone
|
||||
reading the code must not mistake the field for an isolation boundary —
|
||||
the test in #204 pins that.
|
||||
4. **Classification inherits down the page tree.** A new or moved page
|
||||
takes at least its parent's level. Raising is ordinary editorial work;
|
||||
**lowering requires a dedicated capability** in the central permission
|
||||
model and is audited with old value, new value, actor and page.
|
||||
5. **Every output channel carries the marking**, and each is an
|
||||
independently closable issue: web view, browser print, server-side PDF,
|
||||
DOCX/ODT, Markdown ZIP, feeds, public API, search results, no-JS shell,
|
||||
attachment download. A channel that cannot carry it internally
|
||||
(arbitrary binary attachments) is marked externally — filename prefix
|
||||
plus companion file — and the remaining gap is a documented residual
|
||||
risk, not a silent one.
|
||||
6. **Unclassified content shows no marking.** Marking everything trains
|
||||
users to ignore markings.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Ten issues, because there are ten output paths; that is the honest cost
|
||||
of "in every output".
|
||||
- The no-JS shell and the SPA are separate render paths, so each needs its
|
||||
own assertion. Likewise the TipTap NodeView path and the server-side
|
||||
`docToHtml` path differ structurally.
|
||||
- The field is a precondition for the read-access audit trail (ADR 0023),
|
||||
which is scoped to classified content only.
|
||||
- Attachments inherit their page's classification. The case where the
|
||||
page link is not yet set (paste-then-insert) fails closed.
|
||||
- Because levels are separated by instance, a page can never "move
|
||||
between levels" inside one deployment — export/import across instances is
|
||||
the path, and its marking is covered by the export channels.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#204 (field + default), #205 (inheritance + downgrade right), #206 (web),
|
||||
#207 (print), #208 (PDF), #209 (DOCX/ODT), #210 (Markdown ZIP), #211
|
||||
(feeds/API/search/no-JS), #212 (attachments), #213 (upload warning).
|
||||
70
docs/architecture/adr/0023-read-access-audit-trail.md
Normal file
70
docs/architecture/adr/0023-read-access-audit-trail.md
Normal file
@ -0,0 +1,70 @@
|
||||
# ADR 0023: Read-access audit trail limited to classified content
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
The existing audit trail (`audit_log`, issue #86) is deliberately scoped to
|
||||
"who changed access or configuration", and its own service comment states
|
||||
that content activity stays log-only. There is no record of _reads_.
|
||||
|
||||
For "operable in an approved environment", read logging is not a mandatory
|
||||
product feature — evidence collection can be a platform function. In
|
||||
practice platform logging cannot answer the question that matters: a proxy
|
||||
log knows URLs, not classifications, so it cannot say which _classified_
|
||||
page was read. Leistungsbeschreibungen tend to list this as a must.
|
||||
|
||||
Two variants were considered. Variant B logs all reads (18–20 AT) and
|
||||
brings volume, latency and retention problems, plus the requirement that no
|
||||
event may be lost. Variant A logs reads of classified pages only (8–10 AT).
|
||||
|
||||
A live editing session is the volume hazard: Yjs sync means continuous
|
||||
traffic per open document.
|
||||
|
||||
## Decision
|
||||
|
||||
**Variant A: read events are recorded only for pages with
|
||||
`classification = VS_NFD`.** Requires ADR 0022.
|
||||
|
||||
1. **All read channels are instrumented**, or the feature is worthless:
|
||||
SPA page fetch, public API GET, attachment download, export, no-JS
|
||||
shell, collab WS join.
|
||||
2. **A dedup window** (session + page + channel within N minutes = one
|
||||
event) keeps Yjs sync from flooding the trail. The recorded event states
|
||||
that it represents a window, so the evidence is not overread.
|
||||
3. **Its own table**, with time partitioning and its own retention period —
|
||||
independent of `audit_log`, because volume, purpose and legal basis all
|
||||
differ.
|
||||
4. **Failure is not silent.** `AuditService` swallows write failures by
|
||||
design; for classified reads a lost event is a gap in evidence, so the
|
||||
behaviour is either hard failure or an explicitly documented
|
||||
degradation. Which one is decided in #222 and stated in the security
|
||||
documentation.
|
||||
5. **Switchable, with a written purpose limitation.** Off means nothing is
|
||||
written anywhere; a startup log line states the trail is off so a gap is
|
||||
never ambiguous.
|
||||
6. **Variant B is rejected**, and the rejection is recorded rather than
|
||||
left open: unbounded volume, the no-loss requirement, and a purpose
|
||||
limitation that is much harder to defend.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The scope limit is the feature's strongest argument in the works-council
|
||||
discussion at the customer: only classified content is observed.
|
||||
- Reads of unclassified content are not evidenced. Deliberate, and it goes
|
||||
into the residual-risk list.
|
||||
- The collab WS join is the awkward channel: authorization there is
|
||||
token-only (signature plus `pageId` match) and the collab server has no
|
||||
permission context. Either the event carries what the token asserts, or
|
||||
the api emits it at token issuance. #222 decides and documents; the
|
||||
choice affects what the trail can prove about live sessions.
|
||||
- Retention and partition maintenance are operational obligations that
|
||||
must ship with the feature, not after it.
|
||||
- Classification at read time is stored with the event: a later
|
||||
reclassification must not rewrite history.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#222 (instrumentation), #223 (dedup window), #224 (table, retention,
|
||||
partitioning), #225 (switch + purpose limitation). Depends on #204/#205.
|
||||
@ -0,0 +1,63 @@
|
||||
# ADR 0024: Reproducible offline deployment
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
A VS zone has no internet egress. "Should work offline" is the answer that
|
||||
loses a first meeting; "tested, here is the procedure" is the one that
|
||||
wins it — which is why the plan pulled this out of the roadmap into
|
||||
Phase 1.
|
||||
|
||||
The current state is favourable but unverified. There is no telemetry, no
|
||||
update check, no CDN; fonts are self-hosted (ADR 0016); CSP is
|
||||
`default-src 'self'`; search is Postgres rather than an external engine;
|
||||
the drawio plugin is vendored rather than loaded from a remote editor. What
|
||||
is missing is evidence, plus two real gaps: images are referenced by tag
|
||||
(including the floating `gotenberg/gotenberg:8`), and there is no
|
||||
documented mirror or update path.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **All third-party images are pinned by digest** (`name:tag@sha256:…`).
|
||||
The tag stays for human readability; the digest decides what runs. A CI
|
||||
check rejects any un-digested third-party reference.
|
||||
2. **The image list is generated, not hand-maintained**, so a mirror
|
||||
procedure cannot silently miss a service.
|
||||
3. **An internal registry is the supported source.** Compose takes the
|
||||
registry prefix from configuration; no site edits image references.
|
||||
4. **Reproducibility without network is a stated choice between two
|
||||
paths**: an offline pnpm store enabling `install` + `build` with
|
||||
networking disabled, **or** prebuilt images only with no customer-side
|
||||
build. Either is acceptable; leaving it unstated is not, because it
|
||||
determines whether the customer can patch locally.
|
||||
5. **The airgap claim is proven by a documented run** in a network-isolated
|
||||
environment, covering every function including the export sidecars, and
|
||||
listing every outbound connection attempt observed. This run is the
|
||||
artefact, and it also answers the plan's open question about what breaks
|
||||
offline.
|
||||
6. **The offline update path is part of the decision, not an afterthought**:
|
||||
bundle, verify by digest, back up, apply, verify, roll back — with the
|
||||
irreversibility of migrations stated explicitly.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Digest pinning creates recurring maintenance: security updates now
|
||||
require an explicit, reviewable change. That visibility is the point.
|
||||
- Digest pinning must precede the mirror and update work, so it sits in the
|
||||
`hardening & supply chain` milestone rather than this one.
|
||||
- The isolated test run will surface findings; each becomes its own issue
|
||||
referenced from #220 rather than expanding that issue's scope.
|
||||
- Outbound SMTP is the one connection an authority may or may not permit;
|
||||
the deployment must be functional without it, and the consequences of
|
||||
disabling it (no notifications, no verification mail — which interacts
|
||||
with `auth.local.enabled = false`) are documented.
|
||||
- CD does not sync stage composes, so digest changes need an explicit
|
||||
rollout step on the stage hosts.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#203 (digest pinning), #218 (registry mirror), #219 (network-free build),
|
||||
#220 (isolated test run), #221 (offline update path), #236 (pinned Node
|
||||
version — added from the Ist-Aufnahme, I-26).
|
||||
61
docs/architecture/adr/0025-plugin-trust-model.md
Normal file
61
docs/architecture/adr/0025-plugin-trust-model.md
Normal file
@ -0,0 +1,61 @@
|
||||
# ADR 0025: Plugin trust model
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
Dorfteich has a plugin architecture with a sandbox (ADR 0008): plugins
|
||||
declare a manifest, run isolated, and hold declared permissions. In a VS
|
||||
zone the question this attracts is blunt — can code execute inside the
|
||||
protected area, and who vouches for it?
|
||||
|
||||
Two facts shape the answer. First, the manifest has no integrity or
|
||||
identity field: nothing binds a bundle to what was reviewed. Second, real
|
||||
code signing needs a signing identity, and without a legal entity behind
|
||||
the project there is none to be had — a self-generated key that we also
|
||||
distribute proves nothing.
|
||||
|
||||
There is also an asymmetry in cost: a hard off-switch is ~2 AT and closes
|
||||
the risk completely for a deployment that does not need plugins; a trust
|
||||
model is 8–10 AT and only _manages_ the risk.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Short term: hard, verifiable off-switch.** `plugins.enabled = false`
|
||||
makes every plugin surface answer 404 — manifests, assets, the frame
|
||||
route, install/uninstall, and the per-pond toggles — following the
|
||||
established pattern of `api.enabled` and `mcp.enabled`. Off is part of
|
||||
the VS-NfD reference configuration.
|
||||
2. **Documents stay readable with plugins off.** An existing plugin block
|
||||
renders its declared `fallback`, never an error. Disabling a feature must
|
||||
not damage content.
|
||||
3. **Medium term: hash pinning, not code signing.** A SHA-256 over the
|
||||
bundle in the manifest, an allowlist of id + pinned hash in
|
||||
`instance_settings`, verification on install and on every load, failing
|
||||
closed. A version bump requires an explicit re-pin.
|
||||
4. **Signing is deliberately rejected for now**, with its reason on the
|
||||
record: no signing identity is available. Should a legal entity exist
|
||||
later, signing becomes an amendment to this ADR, not a new discovery.
|
||||
5. **The sandbox remains the containment mechanism.** Hash pinning answers
|
||||
"is this the reviewed code", not "what may it do". Both are needed and
|
||||
neither substitutes for the other.
|
||||
6. **Network allowlisting for plugins stays unscheduled**, consistent with
|
||||
the existing project decision; in the VS-NfD profile plugins are off, so
|
||||
it is not the binding constraint.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The offer stage can answer the code-execution question with a switch and
|
||||
a test, without waiting for #232.
|
||||
- Vendored third-party plugin code (drawio 30.3.6 under
|
||||
`packages/plugins/drawio/vendor/`) is part of our supply chain and
|
||||
appears in the SBOM (#202). It loads no external editor URL — verified —
|
||||
and CSP would block it if it tried.
|
||||
- Hash pinning makes plugin updates a deliberate act, which is the intended
|
||||
friction.
|
||||
- The plugin ecosystem stays small by construction. Accepted.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#200 (hard off-switch), #232 (allowlist + hash pinning).
|
||||
53
docs/architecture/adr/0026-backup-target-restriction.md
Normal file
53
docs/architecture/adr/0026-backup-target-restriction.md
Normal file
@ -0,0 +1,53 @@
|
||||
# ADR 0026: Backup target restriction
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-07-29
|
||||
|
||||
## Context
|
||||
|
||||
Backups are the largest single egress path in the system: the entire
|
||||
content of the instance, in one artefact. Today the remote destination is a
|
||||
freely configurable WebDAV/Nextcloud URL in `instance_settings`, validated
|
||||
as a URL but not restricted to any host, plus an rsync mirror to a private
|
||||
host (ADR 0015, issue #84). Anyone with Site-Admin can therefore direct a
|
||||
full copy of the instance to an arbitrary server.
|
||||
|
||||
The tempting answer is to encrypt backups in the application. ADR 0019
|
||||
rules that out: media protection is the platform's base function, and
|
||||
implementing it here would move Dorfteich into the certification
|
||||
obligation under §51 VSA.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **A deploy-level allowlist constrains permissible backup
|
||||
destinations.** Deploy-level, not a runtime setting, so a compromised
|
||||
Site-Admin account cannot widen it.
|
||||
2. **An empty allowlist disables every remote target** — WebDAV and rsync
|
||||
mirror alike. "Local only" is the VS-NfD reference configuration.
|
||||
3. **The admin UI distinguishes "unavailable" from "unconfigured"**, so an
|
||||
operator is never left guessing whether a missing backup is a
|
||||
misconfiguration or policy.
|
||||
4. **No application-side backup encryption**, following ADR 0019. Backup
|
||||
media are protected by the platform.
|
||||
5. **Integrity of backup artefacts is in scope**, unlike their
|
||||
confidentiality: checksums let a restore be verified, which is an
|
||||
application concern because only we know what the artefact should
|
||||
contain (see #199 for the same reasoning on attachments).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Existing deployments that use a remote target must have it added to the
|
||||
allowlist, or backups stop. This is a breaking change and is called out
|
||||
in the release notes.
|
||||
- The delimitation statement (#226) must state plainly that backups leave
|
||||
the application unencrypted and that media protection is the operator's
|
||||
duty. That sentence will be read closely; it is the correct one.
|
||||
- Off-site backup in an airgapped deployment becomes an operator process
|
||||
(media handling), not an application feature.
|
||||
- Restore stays unchanged, including the maintenance-mode interlock that
|
||||
closes collab sessions during a restore.
|
||||
|
||||
## Implementing issues
|
||||
|
||||
#192 (allowlist + deploy-level disable). Related: #199 (integrity
|
||||
hashes), #229 (backup/restore chapter of the operations manual).
|
||||
135
docs/vs-nfd/00-analyse-auftrag.md
Normal file
135
docs/vs-nfd/00-analyse-auftrag.md
Normal file
@ -0,0 +1,135 @@
|
||||
# Analyse-Auftrag: VS-NfD-Ist-Aufnahme Dorfteich
|
||||
|
||||
> Diese Datei in das Repo legen (z. B. `docs/vs-nfd/00-analyse-auftrag.md`)
|
||||
> und Claude Code anweisen: _„Arbeite `docs/vs-nfd/00-analyse-auftrag.md` ab."_
|
||||
|
||||
---
|
||||
|
||||
## Rahmen
|
||||
|
||||
**Ziel des Vorhabens:** Dorfteich soll in einer nach VSA freigegebenen
|
||||
IT-Umgebung einer Bundesbehörde betrieben werden können — Einstufung
|
||||
VS-NfD. Es wird **keine** eigene BSI-Zulassung angestrebt.
|
||||
|
||||
**Leitprinzip, an dem alles zu messen ist:**
|
||||
Dorfteich darf **keine Sicherheitsgrundfunktion im Sinne von §52 VSA selbst
|
||||
implementieren**. Verschlüsselung, Authentisierung, Netzabschluss und
|
||||
Datenträgerschutz gehören auf die Plattform der Behörde. Jede Stelle, an der
|
||||
die Anwendung selbst schützt statt zu delegieren, ist ein Befund.
|
||||
|
||||
**Diese Analyse ist read-only.** Keine Codeänderungen, keine Refactorings,
|
||||
keine Bugfixes. Nur Befunde.
|
||||
|
||||
---
|
||||
|
||||
## Arbeitsweise
|
||||
|
||||
- Jeder Befund braucht einen **Fundort**: `pfad/zur/datei.ts:123`.
|
||||
- Wo du unsicher bist, schreib „unklar" statt zu raten. Eine ehrliche
|
||||
Wissenslücke ist brauchbar, eine erfundene Antwort ist gefährlich.
|
||||
- Keine Verbesserungsvorschläge im Fließtext — dafür gibt es die Spalte
|
||||
„Handlungsbedarf".
|
||||
- Bewertungsskala pro Befund:
|
||||
- **OK** — unkritisch, keine Anpassung nötig
|
||||
- **ANPASSEN** — muss geändert werden, aber überschaubar
|
||||
- **BLOCKIEREND** — verhindert den Einsatz, bis es gelöst ist
|
||||
- **UNKLAR** — nicht abschließend bewertbar
|
||||
|
||||
---
|
||||
|
||||
## 1. Sicherheitsgrundfunktionen (höchste Priorität)
|
||||
|
||||
Beantworte präzise, was die Anwendung **selbst** tut:
|
||||
|
||||
1. **Authentisierung** — Gibt es eine eigene Benutzer-/Passwortdatenbank?
|
||||
Welches Hashing-Verfahren? Existiert bereits OIDC-/SAML-/LDAP-Anbindung
|
||||
oder Unterstützung für Client-Zertifikate / Reverse-Proxy-Header?
|
||||
2. **Session-Verwaltung** — Eigene Implementierung oder Framework?
|
||||
Wo liegen Sessions (Cookie, Server, Redis)? Wie lange gültig?
|
||||
3. **Kryptographie** — Wird irgendwo im Code selbst ver-/entschlüsselt
|
||||
oder signiert? Suche nach eigenen Krypto-Aufrufen, nicht nur nach
|
||||
Bibliotheken. Falls ja: Was, womit, warum?
|
||||
4. **Zugriffskontrolle** — Wie ist das Berechtigungsmodell aufgebaut?
|
||||
Wo wird es durchgesetzt (zentral im Middleware-Layer oder verstreut)?
|
||||
Gibt es Pfade, die es umgehen (API, Suche, Export, Anhänge)?
|
||||
5. **Integrität** — Gibt es Prüfsummen, Signaturen, Manipulationsschutz?
|
||||
|
||||
## 2. Datenhaltung
|
||||
|
||||
6. Wo liegen Seiteninhalte — Datenbank, Filesystem, beides?
|
||||
7. Wo liegen Anhänge und hochgeladene Dateien?
|
||||
8. Existiert ein Volltextsuchindex? Welche Technologie, wo persistiert er,
|
||||
und enthält er Klartext der Seiteninhalte?
|
||||
9. Welche weiteren Kopien der Inhalte entstehen im Betrieb — Caches,
|
||||
Thumbnails, Vorschaubilder, Render-Artefakte, Temp-Dateien?
|
||||
10. Wie ist die Revisions-/Versionshistorie abgelegt?
|
||||
|
||||
## 3. Löschen und Vernichtung
|
||||
|
||||
11. Was passiert beim Löschen einer Seite — Soft- oder Hard-Delete?
|
||||
12. Werden dabei erfasst: Revisionen, Anhänge, Suchindex, Caches,
|
||||
Thumbnails, Papierkorb, Backlinks?
|
||||
13. Gibt es einen Weg, eine Seite samt **aller** Spuren rückstandsfrei zu
|
||||
entfernen? Falls nein: Was bleibt konkret übrig und wo?
|
||||
|
||||
## 4. Ausgehende Verbindungen
|
||||
|
||||
14. Liste **jede** Stelle, an der die Anwendung eine Verbindung nach außen
|
||||
aufbaut oder aufbauen könnte: Update-Checks, Telemetrie, Analytics,
|
||||
Crash-Reporting, Lizenzprüfung, Link-Vorschauen, oEmbed, Avatar-Dienste,
|
||||
Karten, externe Schriften, Icons, Skripte.
|
||||
15. Prüfe auch die gepinnten Fremdmodule (Editor, Flowchart) — laden diese
|
||||
zur Laufzeit etwas nach?
|
||||
16. Backups: Wohin kann konfiguriert werden? Gibt es eine Einschränkung oder
|
||||
ist jedes Ziel erlaubt? Werden Backups verschlüsselt, und wenn ja, wie?
|
||||
17. Läuft die Anwendung vollständig ohne Internetzugang? Was bricht?
|
||||
|
||||
## 5. Ausgabekanäle (für die Kennzeichnungspflicht)
|
||||
|
||||
18. Liste alle Wege, auf denen Inhalte die Anwendung verlassen:
|
||||
Web-Ansicht, Druck, PDF-/DOCX-Export, API, Feeds, Suchergebnisse,
|
||||
Anhang-Download, Backup.
|
||||
19. Gibt es bereits ein Metadatenmodell pro Seite, in das eine Einstufung
|
||||
aufgenommen werden könnte? Wie ist es strukturiert, und wird es an die
|
||||
Ausgabekanäle durchgereicht?
|
||||
20. Wo genau müsste eingegriffen werden, damit ein Kopf-/Fußaufdruck in
|
||||
_allen_ Ausgaben erscheint? Nenne die konkreten Stellen.
|
||||
|
||||
## 6. Protokollierung
|
||||
|
||||
21. Was wird heute protokolliert — nur Änderungen oder auch Lesezugriffe?
|
||||
22. Wohin (Datei, stdout, DB)? Ist Syslog-/SIEM-Export möglich?
|
||||
23. Ist die Aufbewahrungsdauer konfigurierbar?
|
||||
24. Landen Inhalte oder personenbezogene Daten in Logs, die dort nicht
|
||||
hingehören?
|
||||
|
||||
## 7. Lieferkette
|
||||
|
||||
25. Erzeuge eine vollständige Abhängigkeitsliste mit Versionen und Lizenzen.
|
||||
26. Welche Abhängigkeiten sind nicht aus EU-/DACH-Quellen oder haben einen
|
||||
unklaren Maintainer-Status?
|
||||
27. Sind alle Versionen tatsächlich gepinnt — auch transitiv (Lockfiles)?
|
||||
28. Wie läuft das Deployment: Container-Images (welche Basis?), Pakete,
|
||||
Quellcode? Wäre eine Offline-Installation möglich?
|
||||
|
||||
## 8. Betriebsmodell
|
||||
|
||||
29. Ist die Anwendung mandantenfähig oder Single-Tenant?
|
||||
30. Welche Konfiguration ist zur Laufzeit änderbar, welche nur beim Deploy?
|
||||
31. Gibt es Funktionen, die ein Betreiber hart abschalten kann (Feature-Flags)?
|
||||
|
||||
---
|
||||
|
||||
## Ergebnis
|
||||
|
||||
Schreibe das Resultat nach `docs/vs-nfd/10-ist-aufnahme.md` mit folgendem
|
||||
Aufbau:
|
||||
|
||||
1. **Management-Zusammenfassung** — max. 15 Zeilen. Wie weit ist Dorfteich
|
||||
vom Ziel entfernt? Was sind die drei größten Brocken?
|
||||
2. **Befundtabelle** — je Zeile: Nr. | Thema | Fundort | Ist-Zustand |
|
||||
Bewertung | Handlungsbedarf | grobe Aufwandsschätzung (S/M/L)
|
||||
3. **Detailbefunde** — pro Kapitel oben, mit Codeauszügen wo hilfreich
|
||||
4. **Offene Punkte** — was du nicht klären konntest und warum
|
||||
|
||||
Sortiere die Befundtabelle nach Bewertung: BLOCKIEREND zuerst.
|
||||
467
docs/vs-nfd/10-ist-aufnahme.md
Normal file
467
docs/vs-nfd/10-ist-aufnahme.md
Normal file
@ -0,0 +1,467 @@
|
||||
# VS-NfD-Ist-Aufnahme Dorfteich
|
||||
|
||||
> Auftrag: `docs/vs-nfd/00-analyse-auftrag.md`. Read-only-Analyse — keine
|
||||
> Codeänderungen.
|
||||
> Stand: 2026-07-29, Code-Stand `main` = `32c8baa` (Prod v0.12.0).
|
||||
> Nachgezogen **nach** dem Maßnahmenplan (`20-massnahmenplan.md`) und dem
|
||||
> Issue-Entwurf (`31-issue-entwurf.md`): der Plan entstand aus einer
|
||||
> Analyse, die nie als Datei abgelegt wurde. Alle Befunde hier sind am
|
||||
> Code verifiziert; wo ein Befund den Plan korrigiert, steht es dabei.
|
||||
|
||||
Leitprinzip der Bewertung: Dorfteich darf **keine Sicherheitsgrundfunktion
|
||||
im Sinne von §52 VSA selbst implementieren**. Verschlüsselung,
|
||||
Authentisierung, Netzabschluss und Datenträgerschutz gehören auf die
|
||||
Plattform der Behörde. Jede Stelle, an der die Anwendung selbst schützt
|
||||
statt zu delegieren, ist ein Befund.
|
||||
|
||||
Bewertung: **OK** · **ANPASSEN** · **BLOCKIEREND** · **UNKLAR**.
|
||||
Aufwand: **S** ≤ 1 AT · **M** 2–3 AT · **L** ≥ 4 AT.
|
||||
|
||||
---
|
||||
|
||||
## 1. Management-Zusammenfassung
|
||||
|
||||
Dorfteich ist näher am Ziel als bei einem Wiki dieser Größe zu erwarten
|
||||
wäre — aber aus einem Grund, der genau benannt werden muss: Es _erbringt_
|
||||
kaum Sicherheitsgrundfunktionen, weil es sie schlicht nicht hat. Keine
|
||||
Inhalts- oder Backup-Verschlüsselung, kein eigenes MFA, keine Krypto
|
||||
jenseits von Argon2id-Credential-Hashing und kurzlebigen Token-Signaturen.
|
||||
Das ist unter §52 VSA die _richtige_ Architektur, nicht eine Lücke — und
|
||||
so ist es zu vertreten.
|
||||
|
||||
Die drei größten Brocken:
|
||||
|
||||
1. **Authentisierung liegt vollständig in der Anwendung.** Es gibt keine
|
||||
OIDC-Anbindung (nur den vorbereiteten `UserIdentity.provider`-Slot) und
|
||||
keinen Weg, die lokale Anmeldung abzuschalten. Solange das so ist,
|
||||
_ist_ Dorfteich Träger einer Sicherheitsgrundfunktion.
|
||||
2. **Es gibt kein Einstufungskonzept.** Seiten haben kein
|
||||
Einstufungsmetadatum, und kein Ausgabekanal kennt einen Aufdruck —
|
||||
Print-CSS fehlt sogar komplett. Kennzeichnung ist die eine
|
||||
VS-NfD-Anforderung, die niemand außer der Anwendung erfüllen kann.
|
||||
3. **Der Offline-/Airgap-Betrieb ist plausibel, aber unbelegt.** Keine
|
||||
Telemetrie, keine CDNs, keine Update-Checks, Fonts self-hosted, drawio
|
||||
vendored — nachweislich. Getestet wurde es nie, und alle Images hängen
|
||||
an Tags statt an Digests (`gotenberg/gotenberg:8` ist ein gleitender
|
||||
Major-Tag).
|
||||
|
||||
Nebenbefunde mit Substanz: Rohdokument-Bytes jedes Im-/Exports bleiben
|
||||
unbefristet in `conversion_jobs` liegen, der Volltextindex enthält
|
||||
Papierkorb-Inhalte, getrashte Teiche werden nie endgültig gelöscht, und
|
||||
Seiteninhalte landen als Yjs-Kopie in der IndexedDB des Endgeräts.
|
||||
|
||||
**42 Befunde: 5 BLOCKIEREND, 21 ANPASSEN, 4 UNKLAR, 12 OK.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Befundtabelle
|
||||
|
||||
Sortiert nach Bewertung. Die Spalte „Handlungsbedarf" nennt in Klammern
|
||||
das Issue aus `31-issue-entwurf.md`, das den Befund adressiert.
|
||||
|
||||
### BLOCKIEREND
|
||||
|
||||
| Nr. | Thema | Fundort | Ist-Zustand | Handlungsbedarf | Aufw. |
|
||||
| ---- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----- |
|
||||
| I-01 | Fremdauthentisierung fehlt | `apps/api/prisma/schema.prisma:562–577`; kein `oidc`-Treffer in `apps/api/src` | `UserIdentity.provider` ist als `"password"` heute / `"oidc:<issuer>"` später dokumentiert. Implementiert ist nur `password`. ADR 0007 erklärt Bereitschaft, nicht Funktion. | OIDC Auth Code + PKCE gegen den vorhandenen Slot (#214) | L |
|
||||
| I-02 | Lokale Auth nicht abschaltbar | `apps/api/src/auth/`, `apps/api/src/settings/instance-settings.service.ts` | Kein `auth.local.enabled`. Muster für harte Schalter existiert (`api.enabled`, `mcp.enabled`, beide Default aus), wird für Auth aber nicht genutzt. Betroffen sind auch Reset, Registrierung, PATs, Feed-Tokens. | Harter Schalter über alle Credential-Flows (#216) | M |
|
||||
| I-03 | Kein Einstufungsmetadatum | `apps/api/prisma/schema.prisma:263–301` | `Page` trägt Titel, Slug, Baumposition, Zeitstempel — kein Feld, das ein Schutzniveau ausdrücken könnte. Labels sind kein Ersatz (I-03a im Detailteil). | Enum-Feld + Vererbung + Herabstufungsrecht (#204, #205) | M |
|
||||
| I-04 | Kein Einstufungsaufdruck in Ausgaben | `apps/web/src` (kein `@media print`), `apps/api/src/import-export/pdf-html.ts:76–79`, `pandoc.converter.ts`, `export-markdown.ts`, `apps/api/src/public/html-shell.ts` | Kein Kanal kennt einen Aufdruck. Print-CSS fehlt **vollständig** — Browserdruck reproduziert die Bildschirmansicht inkl. Navigation. PDF hat einen Kopf **einmalig** statt je Seite; pandoc läuft ohne Reference-Doc, also ohne Kopf-/Fußzeile. | Sieben Kanal-Issues (#206–#212) | L |
|
||||
| I-05 | Airgap unverifiziert, Images nur per Tag | `deploy/compose/docker-compose.yml:186,206,221,237`; `.gitea/workflows/ci.yml` | `postgres:17.5-alpine`, `pandoc/core:3.6`, `gotenberg/gotenberg:8`, `caddy:2.10-alpine` — Tags, keine Digests; `gotenberg:8` gleitet über Minor/Patch. Kein Mirror-Verfahren, kein Offline-Update-Pfad, kein Testlauf. | Digest-Pinning, Mirror, Offline-Build, isolierter Testlauf, Update-Pfad (#203, #218–#221) | L |
|
||||
|
||||
### ANPASSEN
|
||||
|
||||
| Nr. | Thema | Fundort | Ist-Zustand | Handlungsbedarf | Aufw. |
|
||||
| ---- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||
| I-06 | Ein Secret für zwei Zwecke | `packages/shared/src/env.ts:55,142`; `apps/collab/src/index.ts:46`; `apps/api/src/pages/pages.service.ts:383`; `apps/api/src/notifications/digest.service.ts:161`; `notifications.controller.ts:51` | `COLLAB_TOKEN_SECRET` signiert Collab-Tokens **und** Unsubscribe-Tokens. Teil-Trennung existiert textuell über ein `PURPOSE`-Präfix (`unsubscribe-token.ts:14`), nicht über Schlüssel. | HKDF-Subkeys je Zweck (#188) | L |
|
||||
| I-07 | Eigenbau-HMAC-JWT | `packages/shared/src/token-crypto.ts:1,37,74` | Kompakter HS256-JWT handgebaut auf `node:crypto`; nur HS256, Signatur in konstanter Zeit vor jedem Lesen ungeprüfter Felder. Sorgfältig — aber handgeschriebene Krypto in der Vertrauensgrenze. Grund ist dokumentiert: identischer Code in CommonJS-api und ESM-collab. Weder `jose` noch `jsonwebtoken` ist Dependency. | Ersatz durch `jose`, Cross-Runtime-Test als Zaun (#188) | L |
|
||||
| I-08 | CSRF lässt fehlende Header durch | `apps/api/src/auth/auth.guard.ts:108–112` | `assertSameOrigin` nimmt `origin ?? referer`; fehlen **beide**, kehrt die Prüfung ohne Entscheidung zurück. Kommentar nennt die Absicht (Nicht-Browser-Clients; SameSite als eigentliche Abwehr). Fail-open bleibt es trotzdem. | Fail-closed mit dokumentierter Ausnahme für Bearer-Clients (#189) | S |
|
||||
| I-09 | Session 30 Tage, hart kodiert | `apps/api/src/auth/sessions.service.ts:8,31,50`; `apps/api/src/auth/auth.guard.ts:58` | `SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000 // sliding 30 days`, bei jeder Berührung erneuert; Cookie-`maxAge` gleich. Kein Idle-Timeout — `lastSeenAt` wird geschrieben, aber nie als Grenze ausgewertet. Nicht konfigurierbar. | Absolute + Idle-Grenze konfigurierbar, Default deutlich darunter (#190) | M |
|
||||
| I-10 | Feed-Token im Query-Parameter | `apps/api/src/public/public.controller.ts:28,43`; `feed.service.ts:25` | Langlebiges Lese-Credential als `?token=…` — landet in Proxy-Logs, Historie, Referrer. Speicherung ist gehasht (`feed-tokens.service.ts:73`), das Problem ist der Transport. Kein Instanzschalter für Feeds. | Token aus dem Query holen oder `feeds.enabled` (#191) | M |
|
||||
| I-11 | Backup-Ziel frei wählbar | `apps/api/src/settings/instance-settings.service.ts` (`backup.nextcloud.baseUrl`); `apps/api/src/backup/backup-target.service.ts:3,65`; `apps/api/src/admin/backup-admin.service.ts:27,112,146`; `apps/backup/src/mirror.ts` | WebDAV-Ziel ist als URL validiert, aber auf keinen Host beschränkt; zweiter Remote-Pfad ist der rsync-Mirror. Keine Allowlist, kein Deploy-Kill-Switch. Wer Site-Admin hat, kann eine Vollkopie der Instanz umleiten. | Deploy-Allowlist, leere Liste = nur lokal (#192) | M |
|
||||
| I-12 | Kein Pond-Purge | `apps/api/prisma/schema.prisma:175,186,277,432,553`; `apps/api/src/trash/trash.service.ts` | `Pond.deletedAt` bildet den Teich-Papierkorb; endgültiges Löschen gibt es **nur für Seiten** (`purgeNow:96`, `purgeDuePages:104`, `purgePage:121`). `Page.pond`, `Attachment.pond` und `Label.pond` haben keine `onDelete`-Aktion → Prisma-Default `Restrict` blockt das Löschen ohnehin. Getrashte Teiche bleiben unbegrenzt liegen. | Pond-Purge über alle abhängigen Tabellen (#193) | M |
|
||||
| I-13 | Kein Orphan-File-Sweep | `apps/api/prisma/schema.prisma:530–538`; registrierte Jobs in `apps/api/src/*/*.module.ts` | Das Schema sagt es selbst: der `pageId`-Link wird nicht angefasst, wenn ein Bild später aus dem Inhalt entfernt wird — „an orphan-file sweep … is a separate future maintenance job"; „`deletedAt` stays unused for now". Von fünf registrierten Jobs (`version-thinning`, `page-compaction`, `trash-purge`, `data-export-purge`, `notification-digest`) ist keiner der Sweep. | Sweep implementieren, `deletedAt` nutzen oder entfernen (#194) | M |
|
||||
| I-14 | Papierkorb im Suchindex | `apps/api/src/search/postgres-search.provider.ts:41,73,142–143` | Der gewichtete `tsvector` liegt auf `page_content_cache.search_vector`; getrashte Inhalte bleiben **im Index** und werden nur query-seitig ausgeblendet (`p.deleted_at IS NULL`, `po.deleted_at IS NULL`). Ein künftiger Abfragepfad ohne diesen Filter leakt Inhalt. | Vektor beim Trashen leeren, Query-Filter als zweite Ebene behalten (#195) | M |
|
||||
| I-15 | Keine Retention für `audit_log` | `apps/api/prisma/schema.prisma:74–91`; `apps/api/src/audit/audit.service.ts` | Persistenter Trail (`AuditEntry`, Issue #86) wächst unbegrenzt; kein Pruning-Job registriert. | Konfigurierbare Aufbewahrung + Job (#196) | S |
|
||||
| I-16 | Keine Security-Response-Header | `apps/api/src/main.ts`; `apps/api/package.json` | `helmet` ist keine Dependency und kommt in `apps/api/src` nicht vor. `app.enableCors()` wird **nicht** aufgerufen — CORS ist damit implizit restriktiv (keine CORS-Header, Browser blockt cross-origin), aber nirgends als Entscheidung festgehalten. Der Web-Tier hat eine strenge CSP; die api-Antworten selbst sind die Lücke. | HSTS, `X-Content-Type-Options`, `Referrer-Policy`, Frame-/Permissions-Policy, CORS explizit (#197) | S |
|
||||
| I-17 | Kein SBOM, kein Lizenzreport | `.gitea/workflows/ci.yml`; kein `sbom`/`syft`/`cyclonedx` in `.gitea/` oder `package.json` | CI macht install, build, lint, typecheck, test, `i18n:check`. Kein Lieferketten-Artefakt. (Die Lizenzlage selbst ist unkritisch, s. I-37.) | CycloneDX je Image + Workspace, Lizenzreport als Artefakt (#202) | M |
|
||||
| I-18 | Keine Attachment-Integritätshashes | `apps/api/prisma/schema.prisma:541–559`; `apps/api/src/files/file-storage.service.ts:21,25` | `Attachment` hat keine Checksumme; die einzigen Hashes im Schema sind Credential-/Token-Hashes. Dateien liegen unter `UPLOADS_DIR/<pondId>/<fileId>`. Manipulation am Dateisystem ist nicht erkennbar. | SHA-256 beim Upload, Prüfung beim Download, Backfill (#199) | M |
|
||||
| I-19 | Plugins nicht instanzweit abschaltbar | `apps/api/src/settings/instance-settings.service.ts`; `apps/api/src/public-api/public-api.guard.ts:64`; `apps/api/src/mcp/mcp.controller.ts:50,84` | Für Public-API und MCP gibt es harte Schalter mit 404-Semantik, für Plugins nicht. Plugin-Zustand ist installierte Menge + Freischaltung je Teich. „Codeausführung in der VS-Zone" ist damit nicht mit einem Schalter beantwortbar. | `plugins.enabled = false` nach demselben Muster (#200) | M |
|
||||
| I-20 | Ereigniskatalog nicht stabil | `apps/api/src/audit/audit.service.ts:9`; 37 Aufrufstellen in `apps/api/src` | `AuditEvent.action: string` — der Doc-Kommentar nennt es „stable dot-namespaced id", erzwungen wird nichts. 34 verschiedene Ids sind in Gebrauch (`auth.login_failed`, `grant.created`, `plugin.installed`, `settings.changed`, …). Ohne Vertrag brechen SIEM-Regeln beim Update. | Typisierte Union + veröffentlichter Katalog + Zaun-Test (#201) | L |
|
||||
| I-21 | Keine Lesezugriffsprotokollierung | `apps/api/src/audit/audit.service.ts` | Bewusst begrenzt: „Content activity (pages, files, exports, labels) intentionally stays log-only — the trail answers 'who changed access/configuration', not 'who edited what'." Lesezugriffe existieren gar nicht. Das einzige `action: 'read'` (`apps/api/src/mcp/mcp.service.ts:243`) ist ein Permission-Parameter, kein Ereignis. | Lesetrail nur für eingestufte Inhalte, Variante A (#222–#225) | L |
|
||||
| I-22 | Rohdokument-Bytes ohne Pruning | `apps/api/prisma/schema.prisma:746–784`; `apps/api/src/import-export/import-export.module.ts:62` | `ConversionJob.input`/`result` sind die **rohen Dokumentbytes** jedes Im-/Exports. Das Schema nennt sie „transient, not the durable copy" und verweist auf „a later maintenance job". Registriert ist nur `data-export-purge`, und `expiresAt` ist laut Schema „Null for every other job kind, whose result never expires". Für `export_docx`/`import_docx` bleiben die Bytes damit unbefristet. | Pruning für alle Job-Arten; im Löschkonzept (#229) ausweisen | M |
|
||||
| I-23 | `mail_outbox` ohne Retention, mit Seitentiteln | `apps/api/prisma/schema.prisma:683–697`; `apps/api/src/notifications/digest.service.ts:16,125,146` | Die Outbox speichert `textBody`/`htmlBody` dauerhaft; kein Pruning-Job. Digest-Mails enthalten **Seitentitel** und Akteursnamen (`- ${page.pageTitle}: … (${actorNames})`). Transaktionsmails selbst tragen keinen Inhalt (`mail-templates.ts:25–44`: Anrede + i18n-Text + Link). | Retention für `mail_outbox`; im Löschkonzept ausweisen | S |
|
||||
| I-24 | Slug-Residuum nach Purge | `apps/api/prisma/schema.prisma:441–461` | `PageLink.fromPage` kaskadiert, `toPage` ist `onDelete: SetNull` — nach dem Purge bleibt in fremden Seiten eine Zeile mit `target_slug` der gelöschten Seite (`toPageId` genullt). Der Titel/Slug einer eingestuften Seite kann selbst eingestufte Information sein. | Bewusst entscheiden: Zeilen mitlöschen oder als Restrisiko (#231) führen | S |
|
||||
| I-25 | Inhaltskopie auf dem Endgerät | `apps/web/src/editor/use-collab-provider.ts:32,76,134–141,184` | Jede geöffnete Seite wird als Yjs-Dokument in die IndexedDB gespiegelt (`dorfteich-page-<pageId>`, Issue #38, ADR 0003). Beim Verlassen wird sie gelöscht, **wenn** der Server synchronisiert hat (`clearData()`); bei unsynchronisierten Offline-Änderungen bleibt sie absichtlich stehen. Bricht der Browser vorher ab, läuft die Aufräum-Routine nicht. Für Entzugsfälle gibt es `discardLocal` (#39). | Nicht „keine Spuren auf dem Endgerät" behaupten; Endgeräteverschlüsselung als Betreiberpflicht dokumentieren (#226, #231) | S |
|
||||
| I-26 | Node-Version nicht gepinnt | `package.json:7–10` | `"engines": { "node": ">=22" }` ist eine Untergrenze, kein Pin; `packageManager: "pnpm@11.9.0"` ist exakt. Für einen reproduzierbaren Offline-Build fehlt die Node-Festlegung. | Node-Version pinnen und im Build-Verfahren nennen (#219) | S |
|
||||
|
||||
### UNKLAR
|
||||
|
||||
| Nr. | Thema | Fundort | Ist-Zustand | Warum unklar |
|
||||
| ---- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| I-27 | Herkunft/Maintainer-Status der Abhängigkeiten | `pnpm-lock.yaml` (1380 aufgelöste Pakete, 366 davon prod) | Lizenzlage geprüft (I-37), Jurisdiktion nicht. | Die npm-Registry weist keine Rechtsträger oder Jurisdiktionen aus. Eine belastbare Antwort braucht Recherche je Paket; für die Kernpakete (React/Meta, TipTap/überwiegend DE-Umfeld, Yjs, NestJS, Prisma) ist sie machbar, für 366 Pakete nicht mit vertretbarem Aufwand. Vorschlag: Kernabhängigkeiten einzeln, Rest über den SBOM (#202) offenlegen. |
|
||||
| I-28 | Verhalten ohne Internetzugang | s. I-05, I-31 | Kein Codepfad baut eine Verbindung nach außen auf außer Sidecars, SMTP und Backup-Ziel. | Ob eine egress-geblockte Instanz **vollständig** funktioniert, ist eine Laufzeitfrage (Container-Pulls, DNS, Zertifikatsprüfungen, SMTP-Timeouts). Nur durch den Testlauf (#220) beantwortbar. |
|
||||
| I-29 | Wirksamkeit der Sicherheitsschalter bei laufendem Betrieb | `apps/api/src/settings/instance-settings.service.ts` | Der Settings-Cache der api ist in-process; ein DB-Write an `instance_settings` wirkt erst nach api-Neustart. | Für `api.enabled`/`mcp.enabled` ist das dokumentiert und betrieblich beherrschbar. Für einen künftigen `plugins.enabled` (#200) oder einen Auth-Schalter (#216) muss geklärt werden, ob „Schalter umgelegt" auch „sofort wirksam" heißt — sonst ist die Referenzkonfiguration zeitweise unwahr. |
|
||||
| I-30 | Vollständigkeit der Ausgabekanal-Liste | `apps/api/src/import-export/`, `apps/api/src/public/`, `apps/api/src/public-api/`, `apps/api/src/files/` | Gefunden wurden: Web-Ansicht, Browserdruck, PDF (Gotenberg), DOCX/ODT (pandoc), Markdown-Einzeldownload, Pond-ZIP, Obsidian-Vault-Export, Atom-Feeds, Public-API, MCP, Suchergebnisse, No-JS-Shell, Anhang-Download, Datenexport (DSGVO), Backup. | Die Liste ist durch Codelesen entstanden, nicht durch eine erschöpfende Routen-Enumeration. Der vorhandene Route-Enumeration-Test des Permission-Modells wäre die Grundlage, das mechanisch zu belegen — empfohlen als Teil von #211. |
|
||||
|
||||
### OK — Stärken, die zu belegen sind
|
||||
|
||||
| Nr. | Thema | Fundort | Ist-Zustand |
|
||||
| ---- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| I-31 | Keine ausgehenden Verbindungen | `apps/api/src/health/readiness.service.ts:83,102`; `apps/api/src/import-export/gotenberg.renderer.ts:59,85`; `pandoc.converter.ts:102,121`; `apps/api/src/mail/smtp-config.service.ts:54,71`; `apps/backup/src/remote.ts`, `mirror.ts` | **Jeder** ausgehende Aufruf im Code geht an einen internen Sidecar (`PANDOC_URL`, `GOTENBERG_URL`), an SMTP oder an das konfigurierte Backup-Ziel. Keine Telemetrie, keine Update-Checks, keine Analytics, kein Crash-Reporting, keine Lizenzprüfung, keine Link-Vorschauen, kein oEmbed, keine Avatar-Dienste, keine externen Karten, Fonts self-hosted (ADR 0016). Das ist der stärkste Einzelbefund des ganzen Berichts. |
|
||||
| I-32 | Passwort-Hashing | `apps/api/src/users/password.ts:1,9–25` | Argon2id über die `argon2`-Bibliothek, mit `needsRehash`-Pfad. Kein Eigenbau. |
|
||||
| I-33 | Zentrales Berechtigungsmodell | `apps/api/src/permissions/`; `apps/api/src/mcp/mcp.service.ts:232–253` | Default-closed, deny-wins, zentral durchgesetzt, mit Route-Enumeration-Test. Der MCP-Endpoint **divergiert nicht**: er ruft `PermissionService.hasPondRole` / `canAccessPage`; eigenständig sind nur seine Schalter. Korrigiert eine offene Frage des Maßnahmenplans. |
|
||||
| I-34 | Volltextsuche ohne externe Engine | `apps/api/src/search/postgres-search.provider.ts:40–41` (ADR 0010) | Postgres-`tsvector`. Kein Elasticsearch, kein zweiter Datenhalter, keine weitere Netzwerkgrenze. |
|
||||
| I-35 | Single-Tenant | `apps/api/prisma/schema.prisma` (33 Modelle, kein `Tenant`/`Organization`) | Teiche sind Container innerhalb _einer_ Instanz. Passt zur empfohlenen Betriebsform „eine Instanz je Einstufungsniveau". |
|
||||
| I-36 | Service Worker cached keine Inhalte | `apps/web/vite.config.ts:12–20` | VitePWA precacht ausschließlich Build-Assets; **kein** Runtime-Caching, `navigateFallbackDenylist` schließt `/api` und `/collab` aus. API-Antworten landen nie im SW-Cache. |
|
||||
| I-37 | Lizenzlage durchweg permissiv | `pnpm licenses list --prod` | 366 Produktionspakete, 11 verschiedene Lizenzen: MIT 301, ISC 24, Apache-2.0 22, BSD-3-Clause 5, BlueOak-1.0.0 4, BSD-2-Clause 4, MIT-0 2, Python-2.0 1, CC0-1.0 1, 0BSD 1, `(MPL-2.0 OR Apache-2.0)` 1. **Kein GPL/AGPL, kein Copyleft mit Verteilungsfolgen.** Projektlizenz MIT. |
|
||||
| I-38 | Log-Redaction | `apps/api/src/app.module.ts:84–93`; `apps/collab/src/logger.ts` | pino-JSON auf stdout, `LOG_LEVEL` konfigurierbar, `redact: { paths: ['req.headers.authorization','req.headers.cookie'], remove: true }`. Kein Datei- oder Syslog-Transport — Weiterleitung ist Sache der Container-Runtime, was für SIEM-Anbindung die richtige Arbeitsteilung ist. |
|
||||
| I-39 | Public-API und MCP standardmäßig aus | `apps/api/src/settings/instance-settings.service.ts` (`api.enabled` / `mcp.enabled`, je `.default(false)`); `public-api.guard.ts:64`; `mcp.controller.ts:50,84` | Beide Oberflächen antworten im ausgeschalteten Zustand mit 404 (Existenz verstecken), zusätzlich muss jeder Teich einzeln zustimmen. Vorbildliches Muster für I-02 und I-19. |
|
||||
| I-40 | drawio ist vendored | `packages/plugins/drawio/vendor/drawio-30.3.6/`; `packages/plugins/drawio/manifest.json` | Kein externer Editor-URL: das Manifest nennt `kind: code`, `permissions: ["blockData","ui"]`, `fallback`, `license: Apache-2.0`; `homepage` ist reines Metadatum. Der drawio-Webapp-Baum liegt im Repo. Korrigiert eine offene Frage des Maßnahmenplans (kein Ausschlusskriterium). |
|
||||
| I-41 | Entzug beendet laufende Sitzungen | `apps/collab/src/index.ts:62–70`; `apps/collab/src/server.ts:108–125`; `apps/api/src/pages/pages.service.ts:57,384` | Collab-Tokens leben **60 Sekunden** (`COLLAB_TOKEN_TTL_SECONDS = 60`); die WS-Ebene autorisiert nur gegen Signatur und `claims.pageId === documentName`, ohne Permission-Kontext. Das ist tragfähig, weil die api bei jedem Token neu prüft und ein Grant-Entzug offene Verbindungen sofort per `pg_notify`-Access-Listener schließt (#39). Für den Lesetrail folgt daraus: die api ist der richtige Emissionsort, mit natürlicher Minutengranularität. |
|
||||
| I-42 | Seiten-Purge ist vollständig | `apps/api/src/trash/trash.service.ts:121–144`; Kaskaden in `apps/api/prisma/schema.prisma:114,313,344,361,373,409,469,486` | `purgePage` löscht Anhänge (Datei + Quota), Content-Cache, Update-Log und Watches explizit; Kommentare, Versionen, Mentions, Pending-Contributors, Labels-Zuordnungen und Favoriten hängen an `onDelete: Cascade`. Kindseiten werden bewusst an den Großeltern-Knoten gehoben. Residuen bleiben nur laut I-24. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Detailbefunde
|
||||
|
||||
### 3.1 Sicherheitsgrundfunktionen
|
||||
|
||||
**(1) Authentisierung.** Es gibt eine eigene Benutzer- und
|
||||
Credential-Haltung: `User` plus `UserIdentity` mit
|
||||
`@@unique([provider, subject])`; `credential` hält den Argon2id-Hash
|
||||
(`schema.prisma:562–577`). Gehasht wird mit `argon2id` über die
|
||||
`argon2`-Bibliothek inklusive `needsRehash` (`users/password.ts:9–25`) —
|
||||
kein Eigenbau, korrektes Verfahren. **Eine OIDC-, SAML- oder
|
||||
LDAP-Anbindung existiert nicht**; ein Suchlauf über `apps/api/src` und
|
||||
`packages/shared/src` findet `oidc` ausschließlich im Schema-Kommentar.
|
||||
Unterstützung für Client-Zertifikate oder vertrauenswürdige
|
||||
Reverse-Proxy-Header ist ebenfalls nicht vorhanden. → I-01, I-02.
|
||||
|
||||
**(2) Session-Verwaltung.** Eigene Implementierung, nicht Framework:
|
||||
`Session` als serverseitige Tabelle, deren `id` der SHA-256-Hash des
|
||||
Session-Tokens ist (`schema.prisma:580–595`, `sessions.service.ts:84`) —
|
||||
ein gestohlener DB-Dump gibt keine gültigen Tokens her. Das Cookie ist
|
||||
`httpOnly`, `sameSite: 'lax'`, `secure` in Produktion
|
||||
(`auth.guard.ts:52–58`). Gültigkeit: gleitende 30 Tage, hart kodiert,
|
||||
ohne Idle-Grenze. → I-09.
|
||||
|
||||
**(3) Kryptographie.** Vollständige Inventur der eigenen Krypto-Aufrufe:
|
||||
|
||||
| Zweck | Verfahren | Fundort |
|
||||
| ------------------------ | --------------------------------- | ------------------------------------------------------- |
|
||||
| Passwörter | Argon2id | `users/password.ts:9–25` |
|
||||
| Session-Ids | SHA-256 über das Token | `auth/sessions.service.ts:84` |
|
||||
| Auth-Tokens (Mail-Flows) | SHA-256 | `auth/auth-tokens.service.ts:55` |
|
||||
| PATs | SHA-256 | `public-api/api-tokens.service.ts:149` |
|
||||
| Feed-Tokens | SHA-256 | `public/feed-tokens.service.ts:73` |
|
||||
| Collab-Tokens | HS256-JWT, handgebaut, TTL 60 s | `shared/token-crypto.ts:1,37,74`; `pages.service.ts:57` |
|
||||
| Unsubscribe-Tokens | HMAC-SHA-256 mit `PURPOSE`-Präfix | `notifications/unsubscribe-token.ts:14,40` |
|
||||
|
||||
**Nirgends werden Inhalte ver- oder entschlüsselt.** Es gibt keine
|
||||
Backup-Verschlüsselung und keine Verschlüsselung in DB oder Dateisystem.
|
||||
Unter §52 VSA ist das die gewollte Arbeitsteilung. Die beiden
|
||||
Befunde betreffen nicht das _Ob_, sondern die Hygiene: ein Secret für zwei
|
||||
Zwecke (I-06) und handgeschriebenes JWT (I-07).
|
||||
|
||||
**(4) Zugriffskontrolle.** Zentral in `apps/api/src/permissions/`,
|
||||
default-closed mit deny-wins, per Guards durchgesetzt und durch einen
|
||||
Route-Enumeration-Test abgesichert. Die geprüften Umgehungskandidaten sind
|
||||
sauber: der Export filtert über `permissions.filterPages`
|
||||
(`export.service.ts:102–108`), die Suche joint auf lebende Seiten und
|
||||
Teiche (`postgres-search.provider.ts:142–143`), der MCP-Endpoint nutzt den
|
||||
zentralen Service (I-33), und die Collab-WS-Ebene ist über kurzlebige
|
||||
Tokens plus Entzugs-Listener abgesichert (I-41). Public-API und MCP sind
|
||||
zusätzlich zweifach gegated (Instanz + Teich) und standardmäßig aus
|
||||
(I-39). Ratenbegrenzung existiert als Decorator-Mechanik
|
||||
(`rate-limit/rate-limit.guard.ts:24`).
|
||||
|
||||
**(5) Integrität.** Keine Prüfsummen auf Anhängen (I-18), keine
|
||||
Signaturen auf Plugin-Bundles (`manifest.json` hat kein Hash-Feld), kein
|
||||
Manipulationsschutz auf dem `audit_log`. Prüfsummen existieren nur als
|
||||
Token-Hashes, also zur Authentisierung, nicht zur Integritätssicherung von
|
||||
Nutzdaten.
|
||||
|
||||
### 3.2 Datenhaltung
|
||||
|
||||
**(6) Seiteninhalte** liegen ausschließlich in Postgres, aber in
|
||||
**fünf** Repräsentationen:
|
||||
|
||||
1. `pages.ydoc_state` (Bytes) — das lebende Yjs-Dokument
|
||||
2. `page_updates` — das Update-Log; gemergt erst bei Compaction
|
||||
3. `page_versions.ydoc_snapshot` — vollständige, selbstständige Snapshots
|
||||
(AUTO/MANUAL/PRE_RESTORE), damit ein Restore nie vom Update-Log abhängt
|
||||
(`schema.prisma:318–347`)
|
||||
4. `page_content_cache` — `plain_text`, `markdown`, `html`, `outline`
|
||||
(jsonb) — vier abgeleitete **Klartext**-Formen, bei jedem Save
|
||||
erneuert (`schema.prisma:392–411`)
|
||||
5. `page_content_cache.search_vector` — der gewichtete `tsvector`
|
||||
|
||||
Für ein Löschkonzept ist Punkt 4 der wichtigste: der Klartext jeder Seite
|
||||
liegt vierfach vor, unabhängig vom Yjs-Zustand.
|
||||
|
||||
**(7) Anhänge** liegen im Dateisystem unter
|
||||
`UPLOADS_DIR/<pondId>/<fileId>` (`file-storage.service.ts:21,25`,
|
||||
Default `./data/uploads`), mit Metadaten in `attachments`. Keine
|
||||
Verschlüsselung, keine Prüfsumme.
|
||||
|
||||
**(8) Volltextindex:** Postgres-FTS, persistiert **auf derselben Zeile wie
|
||||
der Klartext** (`page_content_cache`), GIN-Index per Raw-SQL-Migration.
|
||||
Er enthält damit zwangsläufig Klartext der Inhalte — und Papierkorb-Inhalte
|
||||
(I-14).
|
||||
|
||||
**(9) Weitere Kopien im Betrieb:**
|
||||
|
||||
| Kopie | Fundort | Lebensdauer |
|
||||
| -------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------ |
|
||||
| `conversion_jobs.input` / `.result` — rohe Dokumentbytes jedes Im-/Exports | `schema.prisma:746–784` | **unbefristet** außer für Datenexport-Jobs (I-22) |
|
||||
| `mail_outbox.text_body` / `.html_body` — Digest-Mails mit Seitentiteln | `schema.prisma:683–697` | **unbefristet** (I-23) |
|
||||
| IndexedDB je geöffneter Seite auf dem Endgerät | `use-collab-provider.ts:76` | bis zum Verlassen, bei unsynchronisierten Änderungen länger (I-25) |
|
||||
| `comments`, `notifications` | `schema.prisma:100–164` | an Seite/Nutzer gekoppelt, kaskadierend |
|
||||
| Backups | `apps/backup/`, ADR 0015 | Retention konfigurierbar |
|
||||
| PWA-Service-Worker | `vite.config.ts:12–20` | **enthält keine Inhalte** (I-36) |
|
||||
|
||||
Thumbnails, Vorschaubilder oder serverseitige Temp-Dateien gibt es
|
||||
nicht: eine Suche nach `tmpdir`/`mkdtemp`/`/tmp` in `apps/api/src` bleibt
|
||||
leer — die Konverter arbeiten über HTTP gegen die Sidecars.
|
||||
|
||||
**(10) Revisionshistorie:** `page_versions` mit vollständigen Snapshots,
|
||||
Trigger-Enum (AUTO/MANUAL/PRE_RESTORE), `contributor_ids` als Array;
|
||||
angelegt von collab bei Sitzungsende und im Bearbeitungsintervall, von der
|
||||
api auf Zuruf. Ausgedünnt durch den registrierten Job
|
||||
`version-thinning`.
|
||||
|
||||
### 3.3 Löschen und Vernichtung
|
||||
|
||||
**(11) Seiten** werden zweistufig gelöscht: `deletedAt` setzt den
|
||||
Papierkorb (ADR 0013), der Job `trash-purge` löscht nach Retention
|
||||
endgültig, zusätzlich gibt es „Purge einzeln" (`trash.controller.ts:26`).
|
||||
|
||||
**(12) Vom Purge erfasst** (I-42): Anhänge inklusive Datei und
|
||||
Quota-Rückgabe, Content-Cache (und damit der Suchvektor), Update-Log,
|
||||
Watches — explizit; Kommentare, Versionen, Mentions,
|
||||
Pending-Contributors, Label-Zuordnungen und Favoriten über
|
||||
`onDelete: Cascade`. Kindseiten werden bewusst an den Elternknoten der
|
||||
gelöschten Seite gehoben.
|
||||
|
||||
**(13) Rückstandsfrei? Nein — vier konkrete Reste:**
|
||||
|
||||
1. `page_links`-Zeilen fremder Seiten behalten den `target_slug` der
|
||||
gelöschten Seite, `to_page_id` wird genullt (I-24).
|
||||
2. `conversion_jobs` behalten die Rohbytes jedes Exports dieser Seite
|
||||
(I-22).
|
||||
3. `mail_outbox` behält Digest-Mails mit dem Seitentitel (I-23).
|
||||
4. IndexedDB-Kopien auf Endgeräten, die die Seite offline geöffnet hatten
|
||||
(I-25).
|
||||
|
||||
Für **Teiche** gibt es überhaupt kein endgültiges Löschen (I-12) — und
|
||||
die FK-Restriktionen würden es derzeit auch blockieren. Backups sind ein
|
||||
fünfter, gewollter Rest mit eigener Retention.
|
||||
|
||||
### 3.4 Ausgehende Verbindungen
|
||||
|
||||
**(14)–(15)** Die vollständige Liste der Stellen, an denen die Anwendung
|
||||
nach außen geht, ist in I-31 aufgeführt: zwei interne Sidecars (pandoc,
|
||||
Gotenberg), SMTP, Backup-Ziel (WebDAV und/oder rsync). **Sonst nichts.**
|
||||
Keine Update-Checks, keine Telemetrie, kein Analytics, kein
|
||||
Crash-Reporting, keine Lizenzprüfung, keine Link-Vorschauen, kein oEmbed,
|
||||
keine Avatar-Dienste, keine externen Karten, Schriften oder Skripte.
|
||||
Die gepinnten Fremdmodule laden zur Laufzeit nichts nach: drawio ist
|
||||
vendored (I-40), Mermaid und Excalidraw sind gebündelte
|
||||
Workspace-Pakete, und die CSP `default-src 'self'` würde einen Nachlader
|
||||
ohnehin blocken.
|
||||
|
||||
**(16) Backups:** Ziel ist frei konfigurierbar, ohne Allowlist; nicht
|
||||
verschlüsselt (I-11). Letzteres ist gewollt (§52 VSA), Ersteres nicht.
|
||||
|
||||
**(17) Betrieb ohne Internet:** Codeseitig nichts entgegen, empirisch
|
||||
unbelegt (I-28). Offene Einzelfrage mit Betriebsrelevanz: ohne
|
||||
ausgehendes SMTP funktionieren Registrierung, Verifikation und
|
||||
Passwort-Reset nicht — was mit `auth.local.enabled = false` (#216)
|
||||
zusammenfällt und dort mitentschieden werden sollte.
|
||||
|
||||
### 3.5 Ausgabekanäle
|
||||
|
||||
**(18)** Gefundene Wege, auf denen Inhalte die Anwendung verlassen
|
||||
(Vollständigkeit s. I-30): Web-Ansicht (SPA), Browserdruck, PDF über
|
||||
Gotenberg, DOCX/ODT über pandoc, Markdown-Einzeldownload
|
||||
(`pages.controller.ts:122`), Pond-ZIP (`export.service.ts:68`),
|
||||
Obsidian-Vault-Export, Atom-Feeds, Public-API, MCP, Suchergebnisse
|
||||
(Snippets), No-JS-Shell (`public/html-shell.ts`), Anhang-Download
|
||||
(`files.controller.ts:51,71`), DSGVO-Datenexport, Backup.
|
||||
|
||||
**(19) Metadatenmodell:** `Page` trägt Titel, Slug, `parentId`, `sortKey`,
|
||||
Zeitstempel, Ersteller — nichts, wo eine Einstufung hineinpasste (I-03).
|
||||
Der naheliegende Ersatz „Labels" trägt nicht, und zwar aus vier
|
||||
belegbaren Gründen (**I-03a**):
|
||||
|
||||
- `Label` ist **teich-gebunden** (`pondId`, `schema.prisma:423–437`) —
|
||||
dieselbe Einstufung wäre in jedem Teich ein anderes Objekt ohne
|
||||
instanzweite Bedeutung.
|
||||
- Labels sind von jedem Editor **bearbeitbar**.
|
||||
- Labels **vererben nicht** im Seitenbaum.
|
||||
- Labels **verlassen die Anwendung nie**: `export.service.ts:98–105` lädt
|
||||
`labelIds` ausschließlich für `permissions.filterPages`; kein
|
||||
Export-Pfad schreibt sie in die Ausgabe. Das korrigiert eine offene
|
||||
Frage des Maßnahmenplans mit einem klaren Nein.
|
||||
|
||||
**(20) Eingriffspunkte für einen Kopf-/Fußaufdruck** — konkret:
|
||||
|
||||
| Kanal | Eingriffsstelle | Zustand |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Web-Ansicht | Seiten-Layout der SPA | kein Aufdruck-Element |
|
||||
| Browserdruck | **fehlt komplett** — kein `@media print` in `apps/web/src` | Neubau nötig, `@page`-Randboxen für Wiederholung je Blatt |
|
||||
| PDF | `import-export/pdf-html.ts:76–79` (Dokumentkopf, einmalig) + Gotenberg-Footer-Template (`:31`, liefert heute Seitenzahlen) | Mechanik für „je Seite" existiert, wird für den Aufdruck nicht genutzt |
|
||||
| DOCX/ODT | `import-export/pandoc.converter.ts` | **kein** `--reference-doc` — es gibt keine Kopf-/Fußzeilendefinition, in die der Aufdruck gehörte |
|
||||
| Markdown-ZIP | `import-export/export-markdown.ts` | keine YAML-Frontmatter-Ausgabe (Frontmatter existiert nur im _Import_, `obsidian-vault.ts:203,528`) |
|
||||
| Atom-Feeds | `public/feed.service.ts` | kein Feld |
|
||||
| Public-API | `public-api/` | Seitenrepräsentation ohne Einstufung |
|
||||
| Suchergebnisse | `search/postgres-search.provider.ts` | Snippets ohne Markierung |
|
||||
| No-JS-Shell | `public/html-shell.ts` | eigener Renderpfad, braucht eigene Behandlung |
|
||||
| Anhang-Download | `files.controller.ts:51,71` | Dateiname/Begleitdatei sind die einzigen Träger |
|
||||
|
||||
### 3.6 Protokollierung
|
||||
|
||||
**(21)** Protokolliert werden Authentisierungs- und Admin-Ereignisse
|
||||
(34 Ids, s. I-20) — persistent in `audit_log` **und** als
|
||||
`audit: …`-stdout-Zeile. Inhaltsaktivität bleibt bewusst log-only,
|
||||
Lesezugriffe fehlen ganz (I-21).
|
||||
|
||||
**(22) Wohin:** pino-JSON auf stdout, kein Datei- oder Syslog-Transport
|
||||
(I-38). Ein SIEM-Export ist damit über die Container-Runtime möglich und
|
||||
braucht keinen anwendungsseitigen Syslog-Client — die Lücke ist nicht der
|
||||
Transport, sondern der stabile Ereigniskatalog (I-20).
|
||||
|
||||
**(23) Aufbewahrungsdauer:** für `audit_log` **nicht** konfigurierbar und
|
||||
ohne Job (I-15); für Backups konfigurierbar
|
||||
(`BACKUP_RETENTION_DAYS`, `backup.localRetentionDays`,
|
||||
`backup.remoteRetentionDays`); `LOG_LEVEL` konfigurierbar, Log-Retention
|
||||
ist Sache der Runtime.
|
||||
|
||||
**(24) Unerwünschte Daten in Logs:** `authorization`- und
|
||||
`cookie`-Header werden entfernt (`app.module.ts:93`); der
|
||||
`AuditEvent.details`-Kommentar verlangt „never secrets, tokens, or page
|
||||
content". Seitentitel und Nutzernamen erscheinen in Logs und
|
||||
Digest-Mails (I-23) — für eine eingestufte Seite kann bereits der Titel
|
||||
schützenswert sein. Das gehört in die Restrisikoliste, nicht in einen
|
||||
Bugfix.
|
||||
|
||||
### 3.7 Lieferkette
|
||||
|
||||
**(25) Abhängigkeiten:** 1380 aufgelöste Pakete im Lockfile, davon **366
|
||||
im Produktionsbaum**. Direkte Produktionsabhängigkeiten: api 31,
|
||||
web 26, collab 7, shared 5, backup 5. Die Kernpakete sind
|
||||
NestJS 11, Prisma 6, Postgres-Client `pg` 8, Argon2, nodemailer 9,
|
||||
pino 9, Yjs 13 mit Hocuspocus 4, TipTap 3 und ProseMirror, React 19,
|
||||
`@modelcontextprotocol/sdk` 1. Lizenzverteilung s. I-37 — durchweg
|
||||
permissiv.
|
||||
|
||||
**(26) Herkunft:** nicht belastbar beantwortbar (I-27).
|
||||
|
||||
**(27) Pinning:** `pnpm-lock.yaml` ist eingecheckt und deckt transitiv
|
||||
alle 1380 Pakete; CI installiert mit `--frozen-lockfile`. Die
|
||||
Manifest-Ranges sind Caret-Ranges, was in Kombination mit dem Lockfile
|
||||
korrekt ist. Ungepinnt sind: **Container-Image-Digests** (I-05) und die
|
||||
**Node-Version** (I-26). `pnpm` ist exakt gepinnt. Randnotiz ohne
|
||||
Sicherheitsbezug: `zod` läuft in api/shared als ^3, im Web als ^4.
|
||||
|
||||
**(28) Deployment:** Container-Images per Docker Compose
|
||||
(`deploy/compose/docker-compose.yml`) — vier eigene Images (web, api,
|
||||
collab, backup) plus `postgres:17.5-alpine`, `pandoc/core:3.6`,
|
||||
`gotenberg/gotenberg:8`, `caddy:2.10-alpine` (letzteres im Profil
|
||||
`caddy`). Migrationen laufen beim api-Start (`main.ts:runMigrations`,
|
||||
mit Advisory-Lock und einer Warteschleife gegen laufende Restores).
|
||||
Eine Offline-Installation ist plausibel und unbelegt (I-05, I-28).
|
||||
|
||||
### 3.8 Betriebsmodell
|
||||
|
||||
**(29) Mandantenfähigkeit:** Single-Tenant (I-35).
|
||||
|
||||
**(30) Konfiguration.** Zwei klar getrennte Ebenen:
|
||||
|
||||
- **Deploy-Zeit (Env):** `DATABASE_URL`, `APP_BASE_URL`, `PORT`,
|
||||
`UPLOADS_DIR`, `COLLAB_TOKEN_SECRET`, `SMTP_*`, `LOG_LEVEL`,
|
||||
`GOTENBERG_URL`, `PANDOC_URL`, `BACKUP_RETENTION_DAYS`,
|
||||
`MIGRATE_ON_START` (`packages/shared/src/env.ts`).
|
||||
- **Laufzeit (`instance_settings`):** `upload.allowedExtensions`,
|
||||
`upload.svgPolicy`, `api.enabled`, `mcp.enabled`, `backup.*`
|
||||
(Retention, Nextcloud-Ziel, Upload-Zeitplan), `legal.imprint`,
|
||||
`legal.privacyPolicy`, `home.content`, Setup-Abschluss.
|
||||
|
||||
Wichtig für Sicherheitsschalter: der Settings-Cache ist in-process, eine
|
||||
Änderung wirkt erst nach api-Neustart (I-29).
|
||||
|
||||
**(31) Hart abschaltbare Funktionen:** Public-API und MCP (instanzweit,
|
||||
Default aus, plus Zustimmung je Teich, I-39); SVG-Uploads
|
||||
(`upload.svgPolicy: reject`); Dateiendungen per Allowlist; Caddy per
|
||||
Compose-Profil. **Nicht** abschaltbar: lokale Authentisierung (I-02),
|
||||
Plugins (I-19), Feeds (I-10).
|
||||
|
||||
---
|
||||
|
||||
## 4. Offene Punkte
|
||||
|
||||
Was in dieser Analyse **nicht** geklärt werden konnte, mit Grund:
|
||||
|
||||
1. **Jurisdiktion und Maintainer-Status der 366 Produktionspakete**
|
||||
(I-27) — die Registry weist es nicht aus. Vorschlag: Kernpakete
|
||||
einzeln belegen, den Rest über den SBOM offenlegen.
|
||||
2. **Funktionsfähigkeit ohne Internetzugang** (I-28) — nur durch den
|
||||
Testlauf in netzisolierter Umgebung beantwortbar. Der Testlauf ist
|
||||
deshalb selbst ein Arbeitspaket (#220), kein Nachweisdokument.
|
||||
3. **Sofortige Wirksamkeit künftiger Sicherheitsschalter** (I-29) —
|
||||
hängt davon ab, ob `plugins.enabled` und ein Auth-Schalter am
|
||||
in-process-Cache vorbei gelesen werden. Zu entscheiden in #200/#216.
|
||||
4. **Vollständigkeit der Ausgabekanal-Liste** (I-30) — durch Codelesen
|
||||
erstellt, nicht mechanisch belegt. Empfehlung: über den vorhandenen
|
||||
Route-Enumeration-Test absichern.
|
||||
5. **Wirkung von `PRE_RESTORE`-Snapshots und Backups auf ein
|
||||
Löschverlangen** — beides sind gewollte Kopien mit eigener Retention;
|
||||
wie lange ein gelöschter eingestufter Inhalt darin fortlebt, ist eine
|
||||
Frage der Retention-Konfiguration beim Betreiber, nicht des Codes. Für
|
||||
das Löschkonzept (#229) muss die Zahl trotzdem benannt werden.
|
||||
6. **Werden Suchsnippets aus dem Klartext-Cache oder aus dem `tsvector`
|
||||
erzeugt?** Für die Kennzeichnungspflicht (#211) reicht der Befund
|
||||
„Snippets enthalten Seitentext"; für die Frage, ob ein Treffer
|
||||
Inhaltsfragmente an nicht-lesende Nutzer ausgeben könnte, wäre eine
|
||||
genauere Betrachtung von `postgres-search.provider.ts` nötig. Die
|
||||
Berechtigungsprüfung joint auf lebende, sichtbare Seiten — ein
|
||||
Leck ist nicht ersichtlich, aber nicht abschließend geprüft.
|
||||
|
||||
---
|
||||
|
||||
## 5. Verhältnis zum Maßnahmenplan
|
||||
|
||||
Diese Ist-Aufnahme wurde **nach** `20-massnahmenplan.md` erstellt. Sie
|
||||
bestätigt dessen Befunde bis auf drei Korrekturen und liefert vier
|
||||
Ergänzungen, die im Plan fehlen:
|
||||
|
||||
**Bestätigt:** alle Phase-1- und Phase-2-Positionen des Plans, jeweils mit
|
||||
Fundort (Tabelle in Abschnitt 2).
|
||||
|
||||
**Korrigiert:**
|
||||
|
||||
1. `deploy/compose/.env` ist **nicht** im Repo (`.gitignore:5–7`,
|
||||
`git ls-files`) — der Punkt bleibt sinnvoll, aber als Verifikation
|
||||
statt als Leck-Behebung.
|
||||
2. Das **drawio-Plugin lädt keine externe URL** (I-40) — kein
|
||||
Ausschlusskriterium.
|
||||
3. **Labels fließen nicht in Exporte** (I-03a) — die offene Frage des
|
||||
Plans ist mit Nein beantwortet, und der MCP-Gate divergiert nicht
|
||||
(I-33).
|
||||
|
||||
**Ergänzt (im Plan nicht enthalten):**
|
||||
|
||||
| Befund | Warum es zählt |
|
||||
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| I-22 `conversion_jobs` halten Rohdokument-Bytes unbefristet | Direkt löschkonzeptrelevant: eine gelöschte eingestufte Seite lebt in ihrem letzten Export weiter. |
|
||||
| I-23 `mail_outbox` ohne Retention, mit Seitentiteln | Zweite unbefristete Kopie inhaltsnaher Daten. |
|
||||
| I-24 `page_links.target_slug` überlebt den Purge | Der Slug einer eingestuften Seite kann selbst schützenswert sein. |
|
||||
| I-25 IndexedDB-Kopie auf Endgeräten | Inhalte verlassen den Server auf Endgeräte-Datenträger — muss in der Abgrenzungserklärung als Betreiberpflicht (Endgeräteverschlüsselung) auftauchen. |
|
||||
| I-26 Node-Version nicht gepinnt | Blockiert die Reproduzierbarkeitsaussage in #219. |
|
||||
|
||||
Empfehlung: I-22 bis I-26 als fünf zusätzliche Issues in die Meilensteine
|
||||
`M24 — VS-NfD: security quick wins` (I-22, I-23, I-24, I-26) bzw. in die
|
||||
Dokumentation `M30` (I-25) aufnehmen. Das hebt den Umfang von 45 auf
|
||||
**50 Issues**; Aufwand zusätzlich ca. **4–6 AT** (I-22 M, I-23 S, I-24 S,
|
||||
I-25 in #226/#231 enthalten, I-26 S).
|
||||
281
docs/vs-nfd/20-massnahmenplan.md
Normal file
281
docs/vs-nfd/20-massnahmenplan.md
Normal file
@ -0,0 +1,281 @@
|
||||
# Maßnahmenplan VS-NfD — Dorfteich (Rev. 2, volle Tiefe)
|
||||
|
||||
Ziel: **einsetzbar in einer nach VSA freigegebenen Umgebung**, keine eigene
|
||||
BSI-Zulassung. Diese Revision zieht die vormals als Roadmap geführten Punkte
|
||||
in die Planung und weist die Terminwirkung aus.
|
||||
|
||||
Aufwand in **Arbeitstagen (AT)** für eine Person mit Claude Code.
|
||||
Erfahrungswert: Implementierung ist der kleinere Teil, Test und Dokumentation
|
||||
der größere. Die Schätzungen enthalten beides.
|
||||
|
||||
---
|
||||
|
||||
**Stand 2026-07-30:** In Meilensteine `M24`–`M31` und Issues #188–#236
|
||||
überführt (Volltexte und Anlage-Protokoll: `31-issue-entwurf.md`,
|
||||
Befundgrundlage: `10-ist-aufnahme.md`). Issue-Nummern stehen an den
|
||||
Checkboxen, Meilensteine an den Phasen.
|
||||
|
||||
## Phase 0 — Nicht bauen (Leitplanken)
|
||||
|
||||
Diese Dinge machen die Situation **schlechter**, weil sie die Anwendung zur
|
||||
Trägerin einer Sicherheitsgrundfunktion nach §52 VSA machen würden:
|
||||
|
||||
- ❌ Keine eigene Backup-Verschlüsselung — Datenträgerschutz ist Plattformsache
|
||||
- ❌ Keine Verschlüsselung von Inhalten in DB oder Filesystem
|
||||
- ❌ Kein eigenes MFA/TOTP, keine eigene Passwort-Policy-Engine
|
||||
- ❌ Keine neuen Krypto-Primitive
|
||||
- ❌ Keine anwendungsseitige Trennung von Einstufungsniveaus
|
||||
|
||||
Stattdessen: **delegieren und dokumentieren.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Blocker · Summe 30–38 AT
|
||||
|
||||
### P1-1 Fremdauthentisierung + lokale Auth abschaltbar · 10–12 AT
|
||||
|
||||
_Meilenstein: `M27 — VS-NfD: external authentication`_
|
||||
|
||||
- [ ] OIDC Authorization Code + PKCE gegen `UserIdentity.provider` (ADR 0007
|
||||
ausbauen), Keycloak als Referenz-IdP · 5–6 AT · #214
|
||||
- [ ] Alternativpfad vertrauenswürdiger Reverse-Proxy-Header bzw. mTLS-
|
||||
Client-Zertifikat · 2 AT · #215
|
||||
- [ ] **Harter Schalter `auth.local.enabled = false`** inkl. Reset- und
|
||||
Registrierungs-Flows, PATs und Feed-Tokens · 2 AT · #216
|
||||
- [ ] Gruppen-/Rollen-Mapping aus IdP-Claims auf das Permission-Modell · 2–3 AT · #217
|
||||
|
||||
### P1-2 Einstufung als First-Class-Metadatum · 14–18 AT
|
||||
|
||||
_Meilenstein: `M26 — VS-NfD: classification metadata`_
|
||||
|
||||
- [ ] Enum-Feld `classification` an `Page`, Migration, Default aus
|
||||
Instance-Setting · 2 AT · #204
|
||||
- [ ] Vererbung im Seitenbaum, Herabstufung nur mit eigenem Recht + Audit · 3 AT · #205
|
||||
- [ ] Durchreichen in alle Ausgabekanäle · 8–12 AT · #206–#212
|
||||
- Web-Ansicht (Kopf/Fuß) · 1 AT · #206
|
||||
- **Print-CSS** (`@media print`, Kopf/Fuß je Seite) — fehlt komplett · 1 AT · #207
|
||||
- PDF via gotenberg (`pdf-html.ts` Header/Footer-Template) · 1 AT · #208
|
||||
- DOCX/ODT via pandoc (Reference-Doc mit Kopf-/Fußzeile) · 2–3 AT · #209
|
||||
- Markdown-ZIP (Frontmatter + Aufdruck) · 1 AT · #210
|
||||
- Atom-Feeds, Public-API, Suchergebnisse, No-JS-Shell · 2–3 AT · #211
|
||||
- Attachment-Download (Dateiname-Präfix + Begleitdatei) · 1–2 AT · #212
|
||||
- [ ] Warnung/Sperre beim Anhängen an eingestufte Seiten · 1 AT · #213
|
||||
|
||||
### P1-3 Verifizierter Offline-/Airgap-Pfad · 8–10 AT ⟵ neu aus Roadmap
|
||||
|
||||
_Meilenstein: `M28 — VS-NfD: offline/airgap deployment` — das
|
||||
Digest-Pinning (#203) läuft vorgezogen in `M25`_
|
||||
|
||||
Hochgezogen, weil das eine Frage im **ersten** Behördengespräch ist. „Sollte
|
||||
gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung".
|
||||
|
||||
- [ ] Alle Images auf Digest pinnen (schließt den `gotenberg:8`-Punkt ein) · 1 AT · #203
|
||||
- [ ] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
|
||||
- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
|
||||
Prebuilt-Images) · 2–3 AT · #219
|
||||
- [ ] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220
|
||||
- [ ] Offline-Update-Pfad inkl. Migrationen · 2–3 AT · #221
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Billig, hohe Prüfer-Signalwirkung · Summe 22–28 AT
|
||||
|
||||
_Meilensteine: `M24 — VS-NfD: security quick wins`; die nachgezogenen
|
||||
Punkte (#199, #200, #201, #202) in `M25 — VS-NfD: hardening & supply
|
||||
chain`_
|
||||
|
||||
- [ ] **Schlüsseltrennung `COLLAB_TOKEN_SECRET`** per HKDF (zweckgebundene
|
||||
Subkeys) — echter Fund, vor allen Features · 1–2 AT · #188
|
||||
- [ ] **Eigenbau-HMAC-JWT durch `jose` ersetzen** · +2–3 AT · #188 ⟵ neu aus Roadmap
|
||||
_Gebündelt mit der Zeile darüber, weil dieselbe Datei
|
||||
(`packages/shared/src/token-crypto.ts`). Einzeln wären es 5–6 AT._
|
||||
Achtung: Unsubscribe-Tokens leben lang in versandten Mails →
|
||||
Dual-Verify-Fenster einplanen.
|
||||
- [ ] **CSRF fail-closed** — fehlendes Origin _und_ Referer wird derzeit
|
||||
durchgelassen · 1 AT · #189
|
||||
- [ ] **Session-Timeout konfigurierbar**, Default deutlich unter 30 Tagen,
|
||||
separates Idle-Timeout · 1–2 AT · #190
|
||||
- [ ] **Feed-Token raus aus dem Query-Parameter**, alternativ Feeds hart
|
||||
abschaltbar · 2 AT · #191
|
||||
- [ ] **Backup-Ziele einschränkbar** — Allowlist, WebDAV/rsync per Deploy
|
||||
vollständig deaktivierbar · 2 AT · #192
|
||||
- [ ] **Pond-Purge implementieren** — getrashte Ponds bleiben ewig liegen · 3 AT · #193
|
||||
- [ ] **Orphan-File-Sweep** implementieren, `Attachment.deletedAt` nutzen
|
||||
oder entfernen · 2 AT · #194
|
||||
- [ ] **Papierkorb aus dem Suchindex** entfernen statt query-seitig filtern · 2 AT · #195
|
||||
- [ ] **Retention-Job für `audit_log`** · 1 AT · #196
|
||||
- [ ] **Security-Header** (helmet), CORS explizit restriktiv · 1 AT · #197
|
||||
- [ ] **SBOM in CI** (CycloneDX/syft) + Lizenzreport als Artefakt · 1–2 AT · #202
|
||||
- [ ] `deploy/compose/.env` prüfen, Beispieldatei statt Realdatei · 0,5 AT · #198
|
||||
- [ ] **Attachment-Integritätshashes** · +2–3 AT · #199 ⟵ neu aus Roadmap
|
||||
SHA-256-Spalte, Berechnung beim Upload, Prüfung beim Download,
|
||||
Backfill-Migration. Nebennutzen: Orphan-Sweep, Dedup, Backup-Verifikation.
|
||||
- [ ] **Plugins hart abschaltbar** (`plugins.enabled = false`) · +2 AT · #200 ⟵ neu
|
||||
Deckt das Risiko „Codeausführung in der VS-Zone" für den
|
||||
Angebotsstand vollständig ab. Hash-Pinning siehe Phase 4.
|
||||
- [ ] **Syslog/SIEM: Ereigniskatalog** · +3–4 AT · #201 ⟵ neu aus Roadmap
|
||||
Der Code-Anteil ist klein (stdout-JSON reicht meist). Wert liegt im
|
||||
**stabilen Ereigniskatalog**: feste Event-IDs, dokumentierte Semantik
|
||||
und Felder, damit die Behörde SIEM-Regeln schreiben kann.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Beweissicherung / Lesezugriffe · 8–20 AT ⟵ neu aus Roadmap
|
||||
|
||||
_Meilenstein: `M29 — VS-NfD: read-access audit trail` (Variante A)_
|
||||
|
||||
Der aufwändigste der nachgezogenen Punkte, und der mit dem größten
|
||||
Gestaltungsspielraum. Zwei Varianten:
|
||||
|
||||
### Variante A (empfohlen): nur eingestufte Inhalte · 8–10 AT
|
||||
|
||||
Protokolliert werden Lesezugriffe **ausschließlich** auf Seiten mit
|
||||
`classification = VS_NFD`. Setzt P1-2 voraus.
|
||||
|
||||
- [ ] Instrumentierung der Lesepfade: Seitenansicht, Public-API-GET,
|
||||
Attachment-Download, Export, No-JS-Shell, Collab-WS-Join · 4 AT · #222
|
||||
- [ ] Dedup-Fenster (eine Sitzung + eine Seite innerhalb N Minuten = ein
|
||||
Ereignis), sonst erzeugt Yjs-Sync eine Ereignisflut · 2 AT · #223
|
||||
- [ ] Getrennte Tabelle mit eigener Retention und Partitionierung · 2 AT · #224
|
||||
- [ ] Abschaltbar, Zweckbindung dokumentiert · 1–2 AT · #225
|
||||
|
||||
Vorteil über den Aufwand hinaus: Die Zweckbindung ist sauber begründbar
|
||||
(„nur eingestufte Inhalte"), was die Personalrats-Diskussion beim Kunden
|
||||
erheblich entschärft.
|
||||
|
||||
### Variante B: alle Lesezugriffe · 18–20 AT
|
||||
|
||||
Zusätzlich Volumen-, Latenz- und Aufbewahrungsprobleme: gepufferte Schreibung
|
||||
ohne Ereignisverlust (ein verlorenes Ereignis ist eine Lücke in der
|
||||
Beweissicherung), Suchtreffer als eigene Ereignisklasse, Partitionierung
|
||||
zwingend.
|
||||
|
||||
**Einordnung:** Für „einsetzbar in zugelassener Umgebung" ist das kein
|
||||
zwingendes Produktmerkmal — Beweissicherung kann die Plattform erbringen. In
|
||||
der Praxis kann Plattform-Logging aber nicht beantworten, _welche eingestufte
|
||||
Seite_ gelesen wurde (Proxy-Logs kennen URLs, nicht Einstufungen). In
|
||||
Leistungsbeschreibungen taucht das als Muss-Kriterium auf. Deshalb rein —
|
||||
aber in Variante A.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Verbleibende Roadmap
|
||||
|
||||
_Meilenstein: `M31 — VS-NfD: backlog`_
|
||||
|
||||
Nur noch ein Punkt bleibt draußen:
|
||||
|
||||
- **Plugin-Allowlist mit Hash-Pinning** · 8–10 AT · #232
|
||||
Manifest mit SHA-256, Allowlist in `instance_settings`, Prüfung beim Laden,
|
||||
Admin-UI. Bleibt zurückgestellt, weil Phase 2 mit der harten Abschaltung das
|
||||
Risiko bereits schließt — und weil echte Code-Signierung ohne juristische
|
||||
Person ohnehin nicht verfügbar ist. Hash-Pinning ist die richtige Antwort,
|
||||
aber nicht die dringendste.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Dokumentation · 15–20 AT (vorher 12–15)
|
||||
|
||||
_Meilenstein: `M30 — VS-NfD: compliance documentation`_
|
||||
|
||||
Wächst um ca. 25 %, weil jede neue Funktion Handbuch- und Härtungsabschnitte
|
||||
nach sich zieht.
|
||||
|
||||
- [ ] **Abgrenzungserklärung §52 VSA** — welche Sicherheitsgrundfunktionen die
|
||||
Anwendung _nicht_ erbringt und wem sie zufallen. Wichtigstes
|
||||
Einzeldokument. · 3 AT · #226
|
||||
- [ ] **Härtungsleitfaden** mit Referenzkonfiguration „VS-NfD-Betrieb":
|
||||
lokale Auth aus, Public-API aus, MCP aus, Feeds aus, Plugins aus,
|
||||
Backup nur lokal · 3 AT · #227
|
||||
- [ ] **Sicherheitsdokumentation**: Architektur, Datenflüsse, Netzplan,
|
||||
Ports/Dienste, Vertrauensgrenzen · 4 AT · #228
|
||||
- [ ] **Betriebshandbuch**: Installation (inkl. Airgap), Update, Backup/Restore,
|
||||
Löschung und Vernichtung, Rollentrennung · 4–5 AT · #229
|
||||
- [ ] **Zuarbeit IT-Grundschutz** APP.3.1 und CON.11.1, je Anforderung
|
||||
„Produkt / Betreiber / nicht anwendbar" · 3–4 AT · #230
|
||||
- [ ] **Restrisikoliste** mit bewusst offenen Punkten · 1 AT · #231
|
||||
|
||||
---
|
||||
|
||||
## Terminwirkung
|
||||
|
||||
| Block | vorher | Rev. 2 |
|
||||
| ----------------------- | ------------ | ---------------- |
|
||||
| Phase 1 Blocker | 22–28 AT | 30–38 AT |
|
||||
| Phase 2 Billigblock | 15–18 AT | 22–28 AT |
|
||||
| Phase 3 Beweissicherung | — | 8–10 AT (Var. A) |
|
||||
| Phase 5 Dokumentation | 12–15 AT | 15–20 AT |
|
||||
| **Summe** | **49–61 AT** | **75–96 AT** |
|
||||
|
||||
Bei 4 produktiven Tagen pro Woche:
|
||||
|
||||
- **vorher:** ca. 3–3,5 Monate
|
||||
- **Rev. 2 mit Variante A:** ca. **4,5–5,5 Monate**
|
||||
- **Rev. 2 mit Variante B:** ca. **5,5–6,5 Monate**
|
||||
- Plus Phase 4 (Hash-Pinning): weitere ~0,5 Monate
|
||||
|
||||
Der Zuwachs von ~26–35 AT verteilt sich zu etwa zwei Dritteln auf
|
||||
Beweissicherung und Airgap-Verifikation. Beides sind Punkte, nach denen
|
||||
gefragt wird — nicht Punkte, die man erklären muss.
|
||||
|
||||
**Empfehlung zur Reihenfolge:** Termin für den Angebotsstand nicht
|
||||
verschieben. Phase 5 und der Billigblock sind nach ca. 3 Monaten fertig — das
|
||||
genügt, um Gespräche zu führen. Phase 1-3 laufen dahinter weiter. Ein Angebot
|
||||
mit belegter Dokumentation und laufender Umsetzung ist besser als ein
|
||||
fertiges Produkt ohne Gesprächspartner.
|
||||
|
||||
---
|
||||
|
||||
## Zu klärende Punkte aus der Ist-Aufnahme
|
||||
|
||||
- [x] **drawio-Plugin**: geklärt — vendored unter
|
||||
`packages/plugins/drawio/vendor/`, lädt keine externe Editor-URL;
|
||||
kein Ausschlusskriterium (Ist-Aufnahme I-40)
|
||||
- [x] Fließen Labels heute in Exporte? **Nein** — `export.service.ts` lädt
|
||||
`labelIds` nur für die Permission-Filterung (Ist-Aufnahme I-03a)
|
||||
- [ ] Was bricht ohne Internetzugang? → wird durch den Testlauf #220
|
||||
beantwortet (Ist-Aufnahme I-28)
|
||||
- [x] MCP-Gate-Duplikat: **divergiert nicht** — nutzt den zentralen
|
||||
`PermissionService`, eigenständig sind nur die Schalter
|
||||
(Ist-Aufnahme I-33)
|
||||
- [x] Collab-WebSocket: WS-Ebene prüft nur das Token, aber Tokens leben
|
||||
60 s und Grant-Entzug schließt Verbindungen per `pg_notify`;
|
||||
Randbedingung für #222 (Ist-Aufnahme I-41)
|
||||
|
||||
---
|
||||
|
||||
## Ergänzungen aus der Ist-Aufnahme (2026-07-30)
|
||||
|
||||
Befunde der nachgezogenen `10-ist-aufnahme.md`, die in diesem Plan
|
||||
fehlten — als Issues angelegt:
|
||||
|
||||
- [ ] Conversion-Job-Payloads prunen — Rohbytes jedes Im-/Exports liegen
|
||||
unbefristet in `conversion_jobs` · 2 AT · #233 (M24, I-22)
|
||||
- [ ] Retention für `mail_outbox` — Digest-Mails tragen Seitentitel
|
||||
· 1 AT · #234 (M24, I-23)
|
||||
- [ ] `page_links.target_slug`-Residuum nach Purge entscheiden
|
||||
· 0,5 AT · #235 (M24, I-24)
|
||||
- [ ] Node-Version pinnen — Voraussetzung für #219 · 0,5 AT · #236 (M25, I-26)
|
||||
|
||||
Ohne eigenes Issue: IndexedDB-Kopie auf Endgeräten (I-25) — als
|
||||
Akzeptanzkriterium in #226 (Abgrenzungserklärung) und #231
|
||||
(Restrisikoliste) verankert.
|
||||
|
||||
---
|
||||
|
||||
## Vorhandene Stärken (im Angebot nach vorne stellen)
|
||||
|
||||
Nicht ausbauen, sondern **belegen**:
|
||||
|
||||
- Keine Telemetrie, keine Update-Checks, keine CDNs, kein Runtime-Nachladen,
|
||||
Fonts self-hosted, CSP `default-src 'self'` — hier scheitern die meisten
|
||||
Konkurrenzprodukte
|
||||
- Zentrales, default-closed Berechtigungsmodell mit deny-wins und
|
||||
Route-Enumeration-Test — ein prüfbares Artefakt
|
||||
- Volltextsuche in Postgres statt externer Suchmaschine
|
||||
- Single-Tenant — passt zur empfohlenen Betriebsform „eine Instanz pro
|
||||
Einstufungsniveau"
|
||||
- Public-API und MCP zur Laufzeit hart abschaltbar, Default aus
|
||||
- Open Source unter MIT — Quelloffenheit ist im Prüfprozess ein Vorteil
|
||||
- Keine Verschlüsselung im Code = korrekte Architektur, nicht fehlende
|
||||
Funktion. So argumentieren.
|
||||
194
docs/vs-nfd/30-issue-adr-auftrag.md
Normal file
194
docs/vs-nfd/30-issue-adr-auftrag.md
Normal file
@ -0,0 +1,194 @@
|
||||
# Übergabe-Auftrag: Maßnahmenplan → ADRs, Issues, Meilensteine
|
||||
|
||||
> Ablage: `docs/vs-nfd/30-issue-adr-auftrag.md`
|
||||
> Aufruf in Claude Code: _„Arbeite `docs/vs-nfd/30-issue-adr-auftrag.md` ab."_
|
||||
|
||||
---
|
||||
|
||||
## Kontext
|
||||
|
||||
`docs/vs-nfd/20-massnahmenplan.md` enthält den priorisierten Maßnahmenplan für
|
||||
die VS-NfD-Ertüchtigung, abgeleitet aus `docs/vs-nfd/10-ist-aufnahme.md`.
|
||||
Ziel des Vorhabens: Dorfteich soll in einer nach VSA freigegebenen Umgebung
|
||||
einer Bundesbehörde (VS-NfD) betrieben werden können, **ohne eigene
|
||||
BSI-Zulassung**.
|
||||
|
||||
Dieser Auftrag überführt den Plan in die Projektkonventionen: ADRs für
|
||||
Architekturentscheidungen, Issues für Arbeitspakete, Meilensteine als Bündel.
|
||||
|
||||
## Entscheidungen für diesen Auftrag
|
||||
|
||||
| Punkt | Vorgabe |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Sprache ADRs + Issues | **Englisch** (Repo-Konvention). Deutsche Rechtsbegriffe bleiben unübersetzt: VS-NfD, Verschlusssache, VSA, Geheimschutz. |
|
||||
| Codeänderungen | **Keine.** Diese Session erzeugt nur Doku, Issues und Meilensteine. |
|
||||
| Anlegen im Forge | **Erst nach Freigabe.** Siehe Zweistufigkeit unten. |
|
||||
|
||||
## Zweistufiges Vorgehen (verbindlich)
|
||||
|
||||
**Stufe 1 — Entwurf zur Review.** Erzeuge `docs/vs-nfd/31-issue-entwurf.md`
|
||||
mit allen geplanten Meilensteinen, Issues und ADRs im Volltext. Nichts im
|
||||
Forge anlegen. Am Ende der Stufe: kurze Zusammenfassung, wie viele
|
||||
Meilensteine/Issues/ADRs entstehen würden, und Rückfrage an Stefan.
|
||||
|
||||
**Stufe 2 — nach ausdrücklicher Freigabe.** ADR-Dateien schreiben, Labels,
|
||||
Meilensteine und Issues anlegen, Issue-Nummern in den Maßnahmenplan
|
||||
zurückschreiben.
|
||||
|
||||
Grund: ~30 Issues über eine API anzulegen ist mühsam zu korrigieren.
|
||||
|
||||
## Forge ermitteln, nicht raten
|
||||
|
||||
Bestimme aus `git remote -v` und den verfügbaren CLIs (`gh`, `glab`, `tea`,
|
||||
`forgejo-cli`), gegen welche Plattform gearbeitet wird. Prüfe, ob eine
|
||||
Authentifizierung besteht (`gh auth status` o. ä.). Falls unklar oder keine
|
||||
CLI verfügbar: **Stufe 1 vollständig ausführen und dann fragen** — nicht mit
|
||||
`curl` gegen eine geratene API-URL improvisieren.
|
||||
|
||||
---
|
||||
|
||||
## Meilensteine
|
||||
|
||||
Dependency-sortiert, nicht phasen-sortiert. Titel und Reihenfolge übernehmen:
|
||||
|
||||
| # | Meilenstein | Inhalt aus Plan | Aufwand |
|
||||
| --- | ----------------------------------- | ------------------------------------------------------------------------------------------- | -------- |
|
||||
| M1 | `VS-NfD: security quick wins` | Phase 2 ohne die drei nachgezogenen Punkte | 15–18 AT |
|
||||
| M2 | `VS-NfD: hardening & supply chain` | P2 nachgezogen: Attachment-Hashes, Plugin-Abschaltung, Ereigniskatalog, SBOM, Image-Digests | 7–10 AT |
|
||||
| M3 | `VS-NfD: classification metadata` | P1-2 vollständig | 14–18 AT |
|
||||
| M4 | `VS-NfD: external authentication` | P1-1 vollständig | 10–12 AT |
|
||||
| M5 | `VS-NfD: offline/airgap deployment` | P1-3 vollständig | 8–10 AT |
|
||||
| M6 | `VS-NfD: read-access audit trail` | Phase 3, Variante A | 8–10 AT |
|
||||
| M7 | `VS-NfD: compliance documentation` | Phase 5 | 15–20 AT |
|
||||
| M8 | `VS-NfD: backlog` | Phase 4 (Plugin-Hash-Pinning) | 8–10 AT |
|
||||
|
||||
Abhängigkeiten in die Meilenstein-Beschreibung schreiben:
|
||||
M6 setzt M3 voraus. M2 (Image-Digests) sollte vor M5 liegen. M7 läuft
|
||||
parallel und beginnt früh — die Abgrenzungserklärung ist nicht von
|
||||
Implementierung abhängig.
|
||||
|
||||
## Labels
|
||||
|
||||
Anlegen, falls nicht vorhanden:
|
||||
`vs-nfd` · `vs-nfd:blocker` · `effort:S` · `effort:M` · `effort:L` ·
|
||||
`area:auth` · `area:export` · `area:storage` · `area:supply-chain` ·
|
||||
`area:docs`
|
||||
|
||||
---
|
||||
|
||||
## Issue-Vorlage
|
||||
|
||||
Ein Issue pro Checkbox-Zeile des Maßnahmenplans. Unterpunkte mit eigenem
|
||||
Aufwand (z. B. die Ausgabekanäle in P1-2) werden **eigene Issues**, nicht
|
||||
Checklisten in einem Sammel-Issue — sie sind unabhängig abschließbar.
|
||||
|
||||
```markdown
|
||||
**Title:** [VS-NfD] <imperative, concise>
|
||||
|
||||
**Plan reference:** `docs/vs-nfd/20-massnahmenplan.md` → <P1-1 / P2-3 / …>
|
||||
**ADR:** <ADR-00XX or "n/a">
|
||||
**Effort:** <S | M | L> (<n> AT)
|
||||
**Depends on:** <#issue or "—">
|
||||
|
||||
## Context
|
||||
|
||||
Why this matters for VS-NfD operation. One or two sentences.
|
||||
Where relevant, reference the §52 VSA principle: the application must not
|
||||
implement security base functions itself.
|
||||
|
||||
## Current state
|
||||
|
||||
Findings from `10-ist-aufnahme.md`, with file paths. Do not invent paths —
|
||||
copy only what the Ist-Aufnahme actually cites, and drop line numbers that
|
||||
were not verified there.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] verifiable, testable statements
|
||||
- [ ] including the required test
|
||||
- [ ] including the documentation touched (operations manual / hardening guide)
|
||||
|
||||
## Out of scope
|
||||
|
||||
What explicitly does not belong here.
|
||||
```
|
||||
|
||||
Regeln:
|
||||
|
||||
- **Keine erfundenen Fundorte.** Nur Pfade aus der Ist-Aufnahme übernehmen.
|
||||
Wo diese keine verifizierte Zeilennummer nennt, ohne Zeilennummer zitieren.
|
||||
- Jedes Issue nennt die Dokumentation, die es mitzieht. Nicht dokumentierte
|
||||
Funktionen sind für dieses Vorhaben wertlos.
|
||||
- Aufwandsangaben aus dem Plan übernehmen, nicht neu schätzen.
|
||||
|
||||
---
|
||||
|
||||
## ADRs
|
||||
|
||||
Nächste freie Nummer aus `docs/adr/` (o. ä.) ermitteln — vorhanden sind u. a.
|
||||
0007 (UserIdentity/provider) und 0016 (self-hosted fonts). Bestehendes
|
||||
Dateinamens- und Statusschema übernehmen.
|
||||
|
||||
Anzulegen, in dieser Reihenfolge:
|
||||
|
||||
**A. `No security base functions in the application (§52 VSA)`** — Ankerdokument
|
||||
Entscheidung: Die Anwendung erbringt keine Sicherheitsgrundfunktion i.S.v.
|
||||
§52 VSA. Verschlüsselung, Datenträgerschutz, Netzabschluss und
|
||||
Authentisierung liegen bei der Plattform des Betreibers. Konsequenz: kein
|
||||
eigenes MFA, keine Inhalts- oder Backup-Verschlüsselung, keine neuen
|
||||
Krypto-Primitive. Begründung: hält die Anwendung außerhalb der
|
||||
Zulassungspflicht nach §51 VSA.
|
||||
_Dieses ADR ist zugleich der Rohentwurf der Abgrenzungserklärung aus M7 —
|
||||
entsprechend sorgfältig schreiben._
|
||||
|
||||
**B. `Token crypto: HKDF key separation and vetted JWT library`**
|
||||
Zweckgebundene Subkeys statt gemeinsamem `COLLAB_TOKEN_SECRET`; Ersatz des
|
||||
Eigenbau-HMAC-JWT durch `jose`. Dual-Verify-Fenster für langlebige
|
||||
Unsubscribe-Tokens dokumentieren.
|
||||
|
||||
**C. `External authentication via OIDC; local passwords optional`**
|
||||
Erweitert ADR 0007. Enthält die Entscheidung für einen harten Schalter
|
||||
`auth.local.enabled = false` und die Alternativpfade Proxy-Header / mTLS.
|
||||
|
||||
**D. `Classification as first-class page metadata`**
|
||||
Warum ein eigenes Enum-Feld an `Page` und **nicht** Labels: Labels sind
|
||||
pond-scoped, nutzerbearbeitbar und vererben nicht. Enthält außerdem die
|
||||
zentrale Betriebsentscheidung: **Trennung der Einstufungsniveaus erfolgt
|
||||
außerhalb der Anwendung** (eine Instanz je Niveau), die Kennzeichnung
|
||||
innerhalb. Die anwendungsseitige ACL ist Ordnung, nicht Schutzmechanismus.
|
||||
|
||||
**E. `Read-access audit trail limited to classified content`**
|
||||
Variante A. Begründung: Ereignisvolumen bei Yjs-Sync, saubere Zweckbindung,
|
||||
Entschärfung der Mitbestimmungsfrage beim Betreiber.
|
||||
|
||||
**F. `Reproducible offline deployment`**
|
||||
Image-Digest-Pinning, interne Registry, Offline-Update-Pfad.
|
||||
|
||||
**G. `Plugin trust model`**
|
||||
Kurzfristig harte Abschaltbarkeit; Hash-Pinning statt Code-Signierung, weil
|
||||
ohne juristische Person keine Signaturidentität verfügbar ist.
|
||||
|
||||
**H. `Backup target restriction`**
|
||||
Allowlist für Backup-Ziele; keine anwendungsseitige Verschlüsselung
|
||||
(folgt aus A).
|
||||
|
||||
Jedes ADR verlinkt am Ende die umsetzenden Issues; jedes Issue verlinkt sein ADR.
|
||||
|
||||
---
|
||||
|
||||
## Rückschreiben in den Plan
|
||||
|
||||
Nach Stufe 2: In `20-massnahmenplan.md` hinter jeder Checkbox die Issue-Nummer
|
||||
ergänzen (`· #142`) und je Phase den Meilenstein nennen. Der Plan bleibt der
|
||||
Index, der Forge hält den Zustand.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- [ ] Jede Checkbox-Zeile des Plans hat genau ein Issue (oder ist als
|
||||
bewusst zusammengefasst begründet)
|
||||
- [ ] Jedes Issue hat Meilenstein, Effort-Label, Area-Label und
|
||||
Akzeptanzkriterien
|
||||
- [ ] Jedes Issue mit Architekturbezug verweist auf ein ADR
|
||||
- [ ] Alle zitierten Pfade existieren (`ls`/`grep` stichprobenartig prüfen)
|
||||
- [ ] Summe der AT je Meilenstein stimmt mit der Tabelle oben überein
|
||||
- [ ] `git status`: außer `docs/` nichts geändert
|
||||
3192
docs/vs-nfd/31-issue-entwurf.md
Normal file
3192
docs/vs-nfd/31-issue-entwurf.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user