Skip to main content

Cybersecurity

1.6 Security, Compliance, and Data Protection

The platform applies a zero-trust security model integrating the following controls:

  • mTLS for all internal communications (Istio Service Mesh);
  • SPIFFE/SPIRE identities for workload authentication;
  • Keycloak SSO for centralized access control, MFA enforcement, and short-lived OIDC tokens;
  • Vault/HSM for key management and secret retrieval;
  • Immutable audit logging via Loki and WORM object storage;
  • Cloudflare WAF for perimeter protection;
  • Row-Level Security (RLS) and Kafka partitioning by jurisdiction to enforce data segregation;
  • GDPR compliance through pseudonymisation and data minimisation;
  • DORA alignment via defined RTO/RPO targets and regular resilience testing. Regular independent audits confirm provider compliance with ESMA cloud outsourcing guidelines and the DLT Pilot Regulation.

1.6.1 Data Retention Policy

The DEUSS platform defines data retention policies based on data classification, legal obligations, security requirements, and operational needs, in alignment with GDPR principles and applicable financial regulations. The following retention categories are applied: Regulatory & Compliance Data (KYC/AML records, investor identification, transaction records, audit trails)

  • retained for 5–10 years after the end of the business relationship. Business & Contractual Data (investor portfolios, offers, agreements, onboarding/case data)
  • retained for the duration of the relationship and applicable legal retention period. Security & Audit Logs (user actions, access logs, audit events)
  • retained for 1–5 years, with compliance-critical audit records retained longer where required. Technical & Operational Data (application logs, monitoring, telemetry)
  • retained for 30–180 days unless required for security investigations or incident analysis. Key retention principles:
  • data is classified by category and assigned a defined retention period
  • retention must be justified by legal, regulatory, security, or operational requirements
  • sensitive and compliance-critical records are protected using secure and immutable (WORM) storage where required
  • expired data is automatically deleted or anonymized unless legal or regulatory obligations require extended retention This approach ensures compliance with GDPR, financial regulations, auditability requirements, and secure handling of sensitive platform data.

1.6.2 Privileged Access and System-wide Roles

The DEUSS platform applies strict Role-Based Access Control (RBAC) and least-privilege principles. System-wide access is limited to a minimal set of privileged operational and compliance roles. The following privileged roles may access selected cross-system information:

  • System Administrator — operational configuration, infrastructure, user and platform management
  • Compliance / Audit Role — read-only access to audit logs, transactions, and regulatory records
  • Support / Operations Roles (L1/L2) — limited and controlled troubleshooting access for operational support activities Access to sensitive data (e.g. KYC, financial or compliance-related data) is:
  • strictly role-restricted
  • logged and auditable
  • granted only on a need-to-know basis Partner (tenant/originator) data remains logically isolated and mutual visibility between partners is prohibited by default. All privileged access is protected using strong authentication (Passkeys/MFA), fully audited, and subject to periodic review. Additional controls such as approval workflows or just-in-time (JIT) access may be applied for elevated operations.

1.6.3 Partner Data Isolation and Tenant Access Control

The DEUSS platform enforces strict tenant isolation between partners (originators) to prevent unauthorized visibility of cases, financing offers, and related business data. All business data is associated with a specific partner_id, which serves as the primary tenant isolation boundary across the platform. Key protection mechanisms include:

  • backend-enforced authorization
  • strict RBAC enforcement
  • backend-enforced authorization checks
  • tenant-based filtering in all queries and APIs
  • secure propagation of partner context (partner_id, partner_role) via OIDC/JWT tokens
  • logical data segregation between partners
  • separation of Partner APIs and Admin APIs
  • centralized identity management via Keycloak
  • encryption in transit and at rest Cross-partner access is denied by default and cannot be bypassed at the frontend level. All APIs validate ownership and partner context before granting access to requested resources. Partner status and lifecycle are centrally governed by the Partner Service. Suspended partners are immediately isolated across the platform by revoking API access and preventing continuation of active workflows. Additional safeguards include:
  • immutable audit logging of access attempts & audit logging and monitoring of all privileged operations
  • security and penetration testing focused on tenant isolation
  • optional logical or physical data partitioning for sensitive datasets
  • rate limiting and abuse protection This approach ensures confidentiality of partner data and prevents unauthorized visibility across tenant boundaries. This approach ensures that:
  • partners operate in fully isolated data domains
  • cross-partner visibility is technically prevented
  • privileged administrative access remains controlled and auditable
  • security and compliance requirements are consistently enforced across the platform.

File-lifecycle

745 — Backend-Owned File Lifecycle for Private Files

Companion to #744 (Permission Model). This issue is the file-domain Policy Enforcement Point (PEP) that consumes the Permission Model defined in #744 — do not implement in isolation.
Audited 2026-05-29 against current code. Core findings (IDOR on DraftDocumentFacade, missing expires_at/single_use/allowed_content_type, per-feature validation, the useScoringDocuments bypass) all still hold. Two drifts applied: presigned-URL TTL now differs per service (company-service PT5M vs bond-issuance PT1H), and @AutoLog is now present on company-service document facades but still absent on DraftDocumentFacade. Line-number citations removed (they rot; anchor to class/method).

1. Context

The platform handles private files attached to sensitive business resources — company documents, scoring documents, issuance draft documents, and draft marketing assets. The current upload / download path is split between one shared frontend upload hook, several feature-specific frontend hooks, several backend facades, and a common S3 file manager.

Today the frontend computes helper metadata (SHA-256 checksum, content length, file type) and calls backend init / register endpoints. That logic is useful for UX and for early failure detection — it is not a security boundary. Frontend checks are trivially bypassable.

2. Terminology (use these terms consistently)

TermMeaning
SubjectAuthenticated principal performing the operation (user or service account).
ResourceBusiness entity the file is attached to: Company, Scoring, IssuanceDraft, etc.
Parent ResourceThe owning entity at the root of the authorization chain (a draft document's parent is the IssuanceDraft; an IssuanceDraft's ultimate parent for permission purposes is its Company / Originator).
ActionCRUD verb against the file: create, read, update, delete. Aligned with #744.
Permission ModelThe tuple (Subject, Parent Resource, Action) → allow / deny decided centrally. Defined in #744 and reused here verbatim.
Policy Enforcement Point (PEP)The single backend layer that consults the Permission Model on every file event. There must be exactly one PEP per file action — no per-feature shortcuts.
Upload IDShort-lived, single-use capability token. Scoped to (Subject, Parent Resource, Action, expected_checksum, expected_content_length, allowed_content_type, expires_at).
Object KeyS3 storage key. Opaque to the client; never used as an authorization input.
UsableState in which the file has passed verification and is visible to read operations.
Technical auditSecurity / forensic audit of file events. Distinct from the business UseCase audit already wired on the frontend via X-Use-Case-Id headers.

3. Current State (with code references)

3.1 Backend

  • Shared S3 abstraction is in place but exposes both streamed download and presigned URLs:
    • offchain/common/file-manager/.../FileManager.kt — interface with upload, finishFileUpload, download, delete, generateUploadUrl, generateDownloadUrl.
    • offchain/common/file-manager/.../S3FileManager.kt — implementation (upload, presign PUT, presign GET).
    • Per-service wiring via FileManagerFactory in company-service, bond-issuance-service, registry-service, media-service.
  • Upload IDs are not consistently scoped. Persisted entities carry requested_by, checksum_sha256, content_length, state ∈ {REGISTERED, STARTED}, created_at. They do not carry:
    • expires_at — no expiry tracking.
    • single_use enforcement — implied by record deletion, not by an invariant.
    • allowed_content_type allowlist — content type is stored but not validated as a capability constraint at finalize time.
    • Entities: CompanyDocumentUploadEntity, ScoringDocumentUploadEntity, DraftDocumentUploadEntity, DraftMarketingAssetUploadEntity. ❌ None of the three missing scopes has been added since the original analysis.
    • ⚠️ Type drift: DraftMarketingAssetUploadEntity.checksumSha256 is a ByteArray (bytea column) while the other three upload entities store checksum_sha256 as String. Normalize during the lifecycle work, or the central finalize-verify step has to special-case one entity.
  • Authorization is inconsistent across resource types at the PEP layer:
    • RegisterCompanyDocumentUploadFacade.registerUpload — calls companyValidator.hasEditAccess(companyId, userId) and throws ForbiddenException on deny. Parent-resource check.
    • RegisterScoringDocumentUploadFacade.registerUpload — checks parent company edit access + scoring state CREATED. Parent-resource check.
    • DraftDocumentFacade.registerUpload — only checks issuanceDraftValidator.existsByIdOrNull(...). No Subject-vs-Parent-Resource check.
    • DraftDocumentFacade.upload — validates uploadContext.issuanceDraftId == request.issuanceDraftId. Confirms the Upload ID's draft binding, but does not re-check the Subject's right to edit that draft now.
    • DraftDocumentFacade.downloadStream — validates document.issuanceDraftId == request.issuanceDraftId. Same gap on the read path.
    • IssuanceDraftAuthorizationFacade wraps DraftDocumentFacade but does not perform a permission check when delegating to registerUpload — the wrapping layer exists but the authorization step is not in it. The calling controller is @Secured(IS_AUTHENTICATED) only (any authenticated user, any draft).
  • Presigned URL TTL differs per service (drift). ⚠️
    • bond-issuance-service/.../application.yaml: GET/PUT default PT1H (env-overridable via PUBLIC_API_PRESIGNED_URL_DURATION_GET/PUT).
    • company-service/.../application.yaml: GET/PUT hard-coded PT5M.
    • Company documents thus expire in 5 minutes; bond/draft documents in 60. There is no single platform default, and the bond-issuance window is long enough to outlive a parent-resource permission change (a presigned URL keeps working after the Subject loses access). The original analysis pinned a uniform PT1H; the real state is an undocumented per-service split.
  • Validation is per-feature, not centralized. Checksum, content length, and content type are passed to the S3 SDK at PUT time (S3FileManager.upload) but there is no post-finalize re-verification and no central allowlist enforcement. Each service defines its own MIME enums (DraftDocumentMimeType, CompanyDocumentMimeType, MarketingAssetFileType); the rules drift independently.
  • Technical audit (@AutoLog) is now inconsistent across services (drift). ⚠️
    • ✅ company-service document facades are annotated: RegisterCompanyDocumentUploadFacade, UploadCompanyDocumentFacade, DeleteCompanyDocumentFacade, GetAllCompanyDocumentsFacade.
    • bond-issuance-service's DraftDocumentFacade (and its methods) remain unannotated — draft document create/read/update/delete produce no audit signal.
    • There is still no file-event-specific audit table (@AutoLog writes to the structured log stream, not a queryable technical-audit store — see §5.4). The gap closed on one service and widened the cross-service inconsistency.

3.2 Frontend

  • Shared upload hook is in place and correctly two-step (register → upload):
    • frontend/libs/shared/src/hooks/useFileUploadFactory.tsuseFileUpload<TFileType> with registerMutation then uploadMutation.
    • Configs: createDraftDocumentUploadConfig, createDraftMarketingAssetUploadConfig, createCompanyDocumentUploadConfig.
    • SHA-256 + content length computed in frontend/libs/shared/src/utils/fileUtils.ts (FileUtils.calculateSHA256Checksum, file.size).
    • UseCase audit headers (X-Session-Id, X-Use-Case-Id, X-Use-Case-Instance-Id) are already injected via createAuditedRequestInit — preserve this; the technical audit (this issue) is separate from the UseCase audit.
  • One feature bypasses the shared hook and calls the OpenAPI SDK directly:
    • apps/deuss-frontend/src/pages/company/hooks/useScoringDocuments.ts — instantiates registerScoringDocumentUploadMutation / uploadScoringDocumentFileMutation from the generated SDK and orchestrates the two-step flow by hand. ❌ Still bypassing.
    • Result: a parallel upload path that any lifecycle hardening could miss. Bring it back onto the shared hook.
  • No expiration handling. The shared hook has generic HTTP error handling but no detection of 409 / 410 "Upload ID expired", no automatic re-init, no retry policy. With company-service now at PT5M, a slow scoring/document flow can expire mid-upload; today the user retries manually.

3.3 Concrete risk surface

  • Stale Upload ID reused for a different resource → cross-tenant file replacement.
  • Wrong-owner registration → file attaches under a foreign company_id / originator_id / draft_id.
  • Checksum / content-length mismatch silently tolerated → integrity loss.
  • Presigned download URL keeps working after the Subject loses access to the parent resource → data leak (acute at the PT1H bond-issuance setting).
  • Reused object key → silent overwrite of an existing usable file.
  • Draft document events leave no technical-audit trail (DraftDocumentFacade unaudited) → no forensic record of a foreign-document access.

Realistic impact: data leak (foreign document), file-replacement attack, unauthorized persistence under a foreign resource, unaudited access to private draft files.

4. Goal

Define and enforce a backend-owned file lifecycle in which every Upload ID is a short-lived authorization capability scoped to a specific (Subject, Parent Resource, Action, expected metadata, expires_at). Every subsequent operation (read, update, delete) is re-authorized against the current Permission Model decision for the Parent Resource — independently of who uploaded the file originally. Validation, expiry, and the technical audit are centralized in the shared file manager and the common facades, so no per-feature path can drift.

5. Scope

5.1 File lifecycle (backend-owned, single PEP per action)

  1. Init upload — Caller requests an upload for an explicit Parent Resource and Action (create or update). PEP consults the Permission Model from #744 before issuing any Upload ID or signed URL.
  2. Issue Upload ID — Scoped to (Subject, Parent Resource, Action, expected_checksum, expected_content_length, allowed_content_type, expires_at). Single-use — invalidated on successful finalize, never reused.
  3. Object upload to storage — Optionally via short-lived presigned URL (see 5.3) or via service-streamed PUT.
  4. Verify & register (finalize) — Before marking the file usable, the backend verifies actual checksum, actual size, content length, content type against the allowlist, object presence in storage, and Upload ID validity + single-use state. Registration rejects any attempt to attach the object to a different Resource / Company / Originator / Draft than the one for which the upload was initialized.
  5. Mark usable — File becomes visible to read operations only after finalize succeeds.
  6. Read / Update / Delete — All three actions re-enter the same PEP and re-consult the current Permission Model from #744. Original uploader identity is not a basis for any decision.

5.2 Validation & limits

The backend explicitly rejects: wrong-subject registration; wrong-resource registration (any drift in company_id / originator_id / draft_id / scoring_id); checksum mismatch; content-length mismatch; oversized files (per-resource-type limit, configurable); disallowed content types (allowlist, never a blocklist); expired Upload ID; already-finalized Upload ID. Normalize checksum_sha256 to one representation across all four upload entities so the central verify step has a single code path.

5.3 Presigned URLs

  • For sensitive files, prefer service-streamed download (already used by DownloadCompanyDocumentFacade and the streamed DraftDocumentFacade.downloadStream).
  • Where a presigned URL is unavoidable, TTL must be minutes, not hours, and uniform across services. Today company-service is already PT5M while bond-issuance is PT1H — converge on one documented per-resource value (company-service's PT5M is the right order of magnitude; shrink bond-issuance to match).
  • A presigned URL must not outlive the Subject's access to the Parent Resource. If revocation happens during the TTL window, the backend must have a mechanism (key rotation, signed-URL scoping, or a service-streamed fallback) to honor it.

5.4 Technical audit (distinct from business UseCase audit)

Not the business UseCase audit. The frontend already attaches X-Use-Case-Id / X-Use-Case-Instance-Id headers via createAuditedRequestInit. This section is the security / forensic audit of file events.

Every sensitive file event (create, read, update, delete) is recorded with: subject_id; parent_resource_type, parent_resource_id; action; file_id (where applicable); decision (allow / deny) and reason; timestamp. The technical audit is queryable independently of the structured log stream and is retained per the platform's incident-response policy. (@AutoLog today emits to the log stream only; it covers company-service document facades but not DraftDocumentFacade — close that gap and back it with a queryable store. See the platform-wide audit-trail control in the GOV analysis, 1.3.)

6. Functional & technical requirements

  • All behavior changes land in the shared S3 file manager and the common facades, not per-feature. The PEP is single — duplicating it per service is the regression we are fixing.
  • The existing valid upload flow must keep working through the shared frontend hook (useFileUploadFactory.useFileUpload) without UX regression. The useScoringDocuments hook that currently bypasses the shared hook must be migrated onto it.
  • API contract changes (new fields in upload-init response — e.g. expires_at, allowed content types, max size) are reflected in OpenAPI and the generated clients.
  • Permission Model integration follows #744 exactly. Do not invent a parallel permission API for files.
  • @AutoLog (or the technical-audit equivalent) is applied uniformly across all four document facades, including DraftDocumentFacade.

7. Acceptance criteria

  • Upload ID is single-use, has a server-issued expires_at, and is scoped to (Subject, Parent Resource, Action). It cannot be reused across any of those dimensions.
  • Backend rejects, with explicit error codes: wrong-subject registration, wrong-resource registration, checksum mismatch, content-length mismatch, oversized files, disallowed content types (allowlist), expired Upload ID, already-finalized Upload ID.
  • Private files cannot be read, updated, or deleted without a current Permission Model decision against the Parent Resource — the original uploader identity grants no implicit access. This includes the DraftDocumentFacade paths (registerUpload, upload, downloadStream) and the IssuanceDraftAuthorizationFacade delegation, which today perform no Subject check.
  • File registration cannot attach the object to a company / originator / draft / scoring other than the one for which the upload was initialized.
  • Presigned URL TTLs for private files are uniform across services and measured in minutes (converge bond-issuance PT1H down to the company-service PT5M order of magnitude); configured in application.yaml and documented per resource.
  • checksum_sha256 is stored in one representation across all four upload entities (resolve the DraftMarketingAssetUploadEntity ByteArray vs String drift).
  • Sensitive file events (create / read / update / delete) are recorded in the technical audit (separate from UseCase audit) with subject / parent_resource / action / file_id / decision / reason, uniformly across all document facades including DraftDocumentFacade.
  • The existing valid upload flow works without UX regression via the shared frontend hook; useScoringDocuments is migrated onto the shared hook; the hook detects Upload ID expiry (409/410) and transparently re-inits.

8. Risks & notes

  • This is not solvable by a frontend MIME check or a single patch on a single upload endpoint. Frontend checks are bypassable; per-feature backend checks drift apart over time — already visible between RegisterCompanyDocumentUploadFacade (checks access) and DraftDocumentFacade (does not), and now also in the per-service presigned TTL and the per-service @AutoLog coverage.
  • Tightly coupled to #744 — implement together. The file PEP is a consumer of the Permission Model from #744; the user → company → bond → draft chain is the same authorization graph. The IssuanceDraftAuthorizationFacade is the natural home for the missing draft-document permission check.
  • When introducing Upload ID expiration, plan the frontend retry path in useFileUploadFactory (detect expiry → re-init upload transparently). The shorter company-service PT5M window already makes this user-visible.
  • Shrinking presigned URL TTLs may surface flakiness in long-running frontend flows; pair the TTL change with an FE re-fetch policy.
  • The technical-audit store overlaps the platform-wide Immutable Audit Trail control (GOV analysis 1.3). Build the file-event audit on that store rather than a file-domain-only table.