Storage

DFIRe stores three different kinds of data on configurable storage backends: encrypted evidence attachments, large forensic artifacts that bypass DFIRe's own encryption, and database backups. This page covers the entire storage system — how to configure storage destinations, how to assign them to each purpose, and what each option means for the people uploading and downloading files.

Mental model: Targets, Roles, Tiers

From DFIRe 1.4.0 onwards storage is configured in three layers:

Layer What it is Where you configure it
Storage Target A single configured backend — an S3 bucket, an SMB share, an SFTP server, or the local filesystem. Holds connection details, credentials, optional KMS key, optional quota. Settings → Storage → Storage Targets
Storage Role A binding that says "use this target for this purpose." There are three roles: Encrypted Storage, Direct Storage, and Backup. Settings → Storage → Storage Roles
Storage Tier How a particular file is protected. Files attached to cases land in either the Secure tier (DFIRe encrypts) or the Direct tier (passthrough, backend-managed at-rest). Each tier maps to one role. Decided per upload by the user; the admin enables which tiers are available.

This separation lets you create a target once and reuse it across roles. One S3 bucket can host encrypted attachments under attachments/, large disk images under direct/, and database backups under backups/ at the same time. Or you can use three separate buckets, or three different backends — whatever fits your storage strategy.

What changed from earlier versions. Pre-1.4.0 deployments had a single "active storage backend" that you switched between. The new model lets each purpose pick its own destination independently, so for example evidence attachments can stay on AWS while backups go to a local NAS, without losing access to anything that was uploaded before.

The two attachment tiers

Files attached to cases or evidence items go through one of two pipelines, picked by where the user clicks Upload in the UI:

Tier Encryption File size Appropriate for
Secure Storage
(default, always available)
AES-256-GCM with DFIRe's three-layer key hierarchy. Each 8 MiB chunk is encrypted as it arrives at the application server — the file is never written to disk in plaintext, even briefly. The encrypted chunks are spooled to a temporary file on the server, then transferred as an already-encrypted blob to the bound target. Up to 4 GB per file. Notes, screenshots, exported reports, small evidence files — anything where you want DFIRe to own end-to-end encryption.
Direct Storage
(opt-in, only visible when configured)
At-rest protection delegated to the storage backend (S3 SSE-KMS, SSE-S3, or none; SMB / SFTP operator-managed). No DFIRe-side encryption. No practical limit. Disk images, memory dumps, large archives — where streaming through DFIRe for encryption is impractical at 100+ GB scale.

Direct Storage weakens DFIRe's end-to-end encryption posture. It is off by default. Enable it only if your threat model accepts customer-managed S3 (with or without KMS), an air-gapped SMB share, or an SFTP server on a trusted segment as the at-rest trust boundary. Organisations with strict chain-of-custody requirements should leave the Direct Storage role unbound and route all evidence through Secure Storage.

The Backup role doesn't appear in the case Attachments tab — it's the destination for scheduled and on-demand database snapshots. See Backup & Recovery for the full backup workflow; on this page Backup is mentioned only where relevant to target / role configuration.

Storage Targets

Targets are managed at Settings > Storage > Storage Targets. Click Add target, pick the type, fill in the connection fields, and click Test connection — the test does a real round-trip against the backend and reports the exact step that fails if something is misconfigured. Save once the test passes.

DFIRe ships with one auto-managed target that always exists:

Local Filesystem

The default target. Files live on the application server's local disk under the media_data Docker volume. Created automatically on first run; you can't delete it or change its type, but you can set a quota and bind roles to it.

  • Storage Quota (GB) — optional soft cap enforced by DFIRe. Empty = unlimited.

Local is the right answer for development, single-server deployments, and air-gapped lab installations where the application server has the storage budget for evidence. It's not available as a Direct Storage target — Direct's whole point is offloading large files to a separate, customer-managed system, and pointing it back at the local disk defeats that.

Backup the volume. Local storage is only as durable as your media_data volume. Schedule regular volume snapshots alongside the database. See Backup & Recovery.

S3-Compatible Storage

Works with AWS S3, MinIO, Backblaze B2, Wasabi, DigitalOcean Spaces, and any other provider that speaks the S3 API. Recommended for production deployments with internet access.

Field Description
Name Display name shown in the target list and role dropdowns. Free-form — e.g. "AWS Frankfurt", "B2 Backups".
Endpoint URL The S3-compatible endpoint for your provider. Leave empty for AWS S3. Example for Backblaze B2: https://s3.eu-central-003.backblazeb2.com.
Region AWS region (e.g. eu-north-1) or the region label from your provider's console.
Bucket Name Bucket DFIRe will write into. Must already exist. One bucket can safely host multiple roles — each role uses its own prefix (attachments/, direct/, backups/).
IAM Role / Access Key Tick Use IAM role when running on EC2 / ECS / EKS with an instance profile or task role. Otherwise paste an Access Key ID + Secret Access Key for an IAM user scoped to this bucket.
KMS Key ID (optional) When set, uploads use SSE-KMS with this key. Leave blank to use the bucket's default encryption (SSE-S3 or none). Backblaze B2 users should leave this blank — B2 does not support AWS KMS.
Use SSL/TLS Enable HTTPS connections. Always enable for production.
Verify SSL certificates Validate the server's certificate against the system trust store. Disable only for self-signed development setups.
Storage Quota (GB) Soft cap enforced by DFIRe before each upload. Empty = unlimited.

IAM permissions

Whether you authenticate with an IAM user access key or an instance role, the principal needs these actions on both the bucket and its objects. Substitute your bucket name for YOUR-BUCKET:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DFIReBucket",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketCors",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET"
    },
    {
      "Sid": "DFIReObjects",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET/*"
    }
  ]
}

ListBucket + ListBucketMultipartUploads support the Direct Storage Sync feature and the daily orphan-upload cleanup. GetBucketCors lets the Test button report a clear "missing CORS rule" diagnostic instead of a generic 403. The object-level actions cover the multipart upload lifecycle, downloads via pre-signed URL, and attachment deletion.

If you're using SSE-KMS, also grant kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, and kms:DescribeKey on the KMS key ARN.

Bucket CORS (required only for Direct Storage on S3)

Direct Storage on S3 has the browser upload parts directly to the bucket, so the bucket's CORS policy must allow the DFIRe origin. Encrypted Storage doesn't need this — it goes through the application server. Apply the following rule from the bucket's Permissions tab:

[
  {
    "AllowedOrigins": ["https://dfire.example.com"],
    "AllowedMethods": ["PUT", "GET", "HEAD", "POST", "DELETE"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]
  • AllowedOrigins — the URL the browser loads DFIRe from. List both your dev and prod origins if you have them. Wildcards work but are not recommended.
  • AllowedMethodsPUT for part uploads, POST for completing multipart uploads, GET + HEAD for downloads / preflights, DELETE for cleanup.
  • ExposeHeadersETag exposure is required — without it the browser can't read the part ETag and DFIRe can't complete the multipart upload.

Recommended bucket settings

  • Block Public Access — all four blocks on. DFIRe never needs the bucket to be publicly readable; downloads happen via short-lived pre-signed URLs.
  • Versioning — enabled. Provides a safety net against accidental deletions and ransomware-style overwrites of evidence.
  • Default encryption — SSE-S3 (AES256) at minimum, SSE-KMS if you want customer-managed keys.

Backblaze B2: use the b2 CLI to set CORS, not the web UI preset. The "Share everything with every origin" preset is download-only and will reject PUT preflight requests. Include both the S3 pseudo-operations (s3_put, s3_get, s3_head, s3_delete, s3_post) and the native ones (b2_upload_file, b2_upload_part, b2_download_file_by_name, b2_download_file_by_id).

SMB / CIFS File Share

Works with Windows file servers, Samba, and enterprise NAS devices. Required protocol level is SMB3 with end-to-end encryption — DFIRe refuses the connection if the server can't negotiate SMB3 with encryption, even when the share would otherwise accept SMB1 or unencrypted SMB2.

Field Description
Name Display name shown in the target list and role dropdowns.
Hostname / IP SMB / CIFS server hostname or IP. Reachable from the DFIRe backend container.
Share Name The network share name (e.g. evidence_files). One share can safely host multiple roles — each role uses its own subdirectory.
Username / Password Service-account credentials for the share. The account needs read, write, create, and delete on the share root.
Domain AD domain name, or WORKGROUP for standalone servers and most NAS devices. Domain users get the username automatically rewritten as DOMAIN\user when authenticating.
Storage Quota (GB) Soft cap enforced by DFIRe before each upload. Empty = unlimited.

Docker networking: the DFIRe backend container must be able to reach the SMB server on TCP/445. If using Docker's default bridge network, ensure routing is configured so SMB traffic reaches the share, or move the container to host networking mode.

SFTP / SSH File Transfer

Works with any SSH server that has the SFTP subsystem enabled. Useful when SMB isn't permitted by network policy or when the storage host lives on a remote segment reachable only via SSH.

Field Description
Name Display name shown in the target list and role dropdowns.
Host / Port SFTP / SSH server hostname (or IP) and port. Default port is 22.
Username SSH user. Needs read, write, create, and delete on the active directory.
Authentication Method Username + Password, or SSH Private Key. Pick one — fallback across methods is intentionally not supported (it surprises sysadmins debugging auth).
SSH Private Key (key auth only) Paste a complete OpenSSH-format private key (Ed25519, ECDSA, or RSA). If the key is encrypted, fill the passphrase field. Modern ssh-keygen defaults including AEAD ciphers (aes256-gcm@openssh.com, chacha20-poly1305@openssh.com) are accepted.
Active Directory Working directory on the SFTP server. Each role lives under its own subdirectory of this path.
Host Key Fingerprint Captured automatically the first time you click Test connection (TOFU — trust on first use). Verify out-of-band that the captured fingerprint matches the server's actual host key before saving. If the host key later rotates, delete the target and recreate it to re-trust the new key.
Storage Quota (GB) Soft cap enforced by DFIRe before each upload. Empty = unlimited.

Storage Roles

Roles are the assignment between a target and a purpose. They live at Settings > Storage > Storage Roles as a small set of dropdowns — one row per role, each with a target picker.

Role Accepts What it stores
Encrypted Storage Local, S3, SMB, SFTP The Secure tier. Every case attachment uploaded through the standard "Upload" button. Files are AES-256-GCM encrypted before they leave the application server, then synced to the bound target.
Direct Storage S3, SMB, SFTP The Direct tier. Visible in the case Attachments tab as a separate "Direct Storage" section once the role is bound. Bypasses DFIRe-side encryption; at-rest protection is whatever the backend provides. Local target is rejected by design.
Backup Local, S3, SMB, SFTP Database snapshots and on-demand exports. Covered separately in Backup & Recovery; managed independently of the attachment roles.

Three things to know about the role bindings:

  • One target can serve multiple roles. A single S3 bucket can be the Encrypted Storage target, the Direct Storage target, and the Backup target simultaneously — each role writes under its own prefix and they don't collide.
  • Each role's binding is independent. Switching the Encrypted Storage role to a different target doesn't affect Direct Storage or Backup.
  • Existing files stay where they were uploaded. When you change a role's target, files already uploaded under the old target keep being read from the old target. Only new uploads land on the new target. There is no built-in migration; if you need to move historical data, copy the storage location's contents out-of-band and use the Direct Storage Sync feature to re-register them under the new target.

The Test Direct check

Once a target is bound to the Direct Storage role, the role row exposes a second Test Direct button. The standard target test exercises basic read / write against the backend; Test Direct additionally exercises the browser-side path that Direct uploads actually use — CORS preflight + signed PUT for S3, the passthrough chunk endpoint for SMB and SFTP. Run it before letting users upload large files so any CORS or routing issue surfaces here rather than mid-upload.

What end users see

The case Attachments tab has up to four upload sections, depending on which roles are bound and which file types the user is uploading:

  • Encrypted Storage — the default. Always present. Upload button accepts any file up to 4 GB. Progress shows percentage as encrypted chunks arrive, then transitions to "Syncing to backend" while the already-encrypted blob is transferred from the server's temporary spool to the bound target.
  • Images — image-file attachments. Same Secure Storage backend as Encrypted Storage; rendered as a separate section because images get an inline thumbnail and a grid layout instead of the default file row.
  • Direct Storage — only visible when a Direct Storage target is bound. No file size cap. Progress shows upload percentage, then "Verifying integrity…" while the SHA-256 is recorded. Multiple file selection queues files one at a time per browser tab.
  • Evidence items — each evidence item has its own Attachments tab with the same three sections (Encrypted Storage, Images, Direct Storage). Capabilities are parallel to the case-level tab; the difference is just which row the file is bound to.

Image thumbnails. Every image uploaded to the Images section gets a small thumbnail rendered server-side and stored encrypted in DFIRe's database alongside the attachment row. The thumbnail is independent from the file blob on the storage backend — if the bound Encrypted Storage target is later deleted, the thumbnail will keep rendering in the case view but the full image will fail to load. This is a known rough edge; it'll be addressed in a future release.

For Direct Storage the row state machine is visible to the user:

  • Queued — waiting for the previous file in this tab to finish.
  • Uploading — bytes streaming to the backend, percentage updates live.
  • Finalizing — server is closing the multipart upload (S3) or the chunk stream (SMB / SFTP).
  • Verifying — the server has the file; SHA-256 is being recorded. Download is disabled during this phase.
  • Ready — download button enabled, hash visible.

Navigating away aborts the upload. Leaving the case page entirely (browser back, opening a different case, refreshing) cancels any in-flight upload. Switching tabs within the case (e.g. Attachments → Notes) keeps the upload running. The per-row Cancel button on each upload is the explicit way to stop one cleanly. If a user closes the tab mid-upload without cancelling, the partial file on the storage backend is removed by the daily orphan-sweep within ~24-48 hours.

The Calculate / Verify SHA-256 button

Direct Storage rows show a small button next to the hash:

  • Calculate SHA-256 — appears on rows that have no hash yet (typically files discovered via Sync — see below). Click queues a background task that streams the file from the backend through the SHA-256 hasher and records the result. For files larger than 256 MiB the click opens a confirmation modal warning about backend egress cost.
  • Re-verify — appears on any row that already has a recorded hash, regardless of where it came from (browser-declared at upload time, or computed server-side via a previous Calculate / Re-verify run). Click streams the file back through the server-side hasher and records whether the disk content still matches the stored hash. A mismatch flips the row's status to "Disk mismatch" and writes an audit row capturing both hashes for evidence.
  • Recompute — appears on rows whose hash is "Stale" because Sync detected the underlying file's size or mtime changed.

Path layout

Each role writes under its own predictable, human-readable prefix so operators can find files directly in the storage console or by browsing the share:

# Encrypted Storage
attachments/{tenant_uuid}/{case_id}/{file_uuid}.bin

# Direct Storage — case attachment
direct/{tenant_uuid}/{case_number}/{filename}

# Direct Storage — evidence-item attachment (nested one level deeper)
direct/{tenant_uuid}/{case_number}/{item_short_id}/{filename}

# Backup
backups/dfire_backup_{YYYYMMDD-HH-MM-SS}-{version}-{mode}-{shortid}.enc

Encrypted Storage uses opaque UUIDs because the file content is encrypted — there's no benefit to a human-readable filename. Direct Storage uses the original filename so files are recognisable in the storage console.

  • Tenant UUID scopes files in case multiple deployments share a storage location.
  • Case number (e.g. CASE-2026-003) groups files by case. Case numbers are immutable once assigned.
  • Item short ID (first 8 hex chars of the item UUID, e.g. d2c721ba) groups files by evidence item within their parent case. Only present for item-level attachments.

Out-of-band uploads (Direct Storage Sync)

For files too large to push through a browser — multi-TB disk images coming off an imaging rig, for example — files can be pre-staged directly into the storage location (the S3 bucket, SMB share, or SFTP working directory) and registered with a case via the Sync button on the Direct Storage section.

How it works

  1. Upload your file out-of-band (AWS CLI, b2 CLI, SMB copy, acquisition-tool direct upload, etc.) to the appropriate path:
    • Case attachment: direct/{tenant_uuid}/{case_number}/your-file.ext
    • Evidence item attachment: direct/{tenant_uuid}/{case_number}/{item_short_id}/your-file.ext
  2. Open the case (for case attachments) or the evidence item (for item attachments) in DFIRe. Click Sync on the Direct Storage section.
  3. DFIRe reconciles its records against the storage location at the exact scope you synced from. New files in that scope are registered as attachments; attachment rows whose backing file has been removed out-of-band are cleared.

If the scoped directory doesn't exist yet on the first Sync, DFIRe creates it so you have a target path to drop files into on the next attempt. This applies equally to fresh cases and fresh evidence items.

Discovered files are unhashed by default

Files registered via Sync land with no SHA-256 — DFIRe doesn't download multi-TB files purely to compute a hash for them. The row shows a Calculate SHA-256 button; clicking it queues a background task that streams the file through the hasher. For organisations with strict chain-of-custody requirements this matters: a discovered file's audit trail begins from the moment Sync registered it, not from when the file was created. If the chain has to be unbroken, route uploads through DFIRe instead and don't hand out direct credentials to the storage location.

Sync is authoritative within its scope

Each Sync run treats the storage location as ground truth for the path it's looking at:

  • Files in the storage location but missing from DFIRe → added as discovered attachments (no hash, no uploader).
  • Attachment rows whose backing object has been deleted out-of-band → removed from DFIRe.
  • In-flight uploads (a managed upload still in verifying state) are excluded from reconciliation so a racing Sync doesn't reap a partial file.
  • Strict path match — case sync only looks at the case root and never descends into item subdirectories; item sync stays inside the item folder. Files placed under the wrong path are simply ignored until synced from the matching scope.

Hashing and the trust model

How a Direct Storage attachment's SHA-256 is established depends on how it arrived:

  • Browser-uploaded: the user's browser hashes the file as it slices it for upload (single pass, no extra read), and ships the hash to the server with the final upload completion. DFIRe stores it as the attachment's hash and records a DIRECT_HASH_BROWSER_DECLARED audit row noting the source.
  • Verify on disk: from the row, the user can click Re-verify at any time. DFIRe streams the file back from the storage backend through its own SHA-256 hasher and compares. A match writes SHA256_COMPUTED; a mismatch writes DIRECT_HASH_DISK_MISMATCH, marks the row as "Disk mismatch", and preserves both hashes in the audit row for evidence. The file is not deleted on mismatch — the user investigates.
  • Discovered (via Sync): no hash at registration. The user clicks Calculate SHA-256 when they want one; the task does the same backend stream-and-hash and writes SHA256_COMPUTED on success.
  • Stale: on each Sync run, DFIRe captures the backend's reported file size and modification time and compares against the snapshot taken when the hash was computed. If either differs, the hash is flagged "Stale" — the recorded hash is preserved as a forensic record of the older bytes; the user can recompute to refresh it.

Why not always recompute server-side? At 100+ GB scale, streaming the file back through the server purely to confirm a hash the browser already computed is multi-minute work and (for cloud backends) can incur egress charges. The browser-side hash is computed during the upload pass anyway — trusting it is essentially free. The opt-in Re-verify button is there for cases where independent attestation matters more than the cost.

Downloads

  • Encrypted Storage — the file streams from the bound target through the application server, where it's decrypted on the fly, then to the browser. The browser never sees ciphertext.
  • Direct Storage on S3 — DFIRe issues a short-lived (5 min) pre-signed GET URL and hands it to the browser, which downloads directly from S3. The signed URL stays out of browser history. Downloads are gated behind hash verification — the file isn't downloadable until either the upload hash arrived or a Calculate / Re-verify run completed. Discovered files have no hash by design and are always downloadable.
  • Direct Storage on SMB / SFTP — the file streams through the DFIRe backend to the browser. Same hash gating as S3.

Every download writes a DOWNLOAD audit row, annotated with how the download was served (presigned_get for S3, smb_passthrough or sftp_passthrough for the others). The recorded hash is captured into the audit row at download time.

Lifecycle

Deleting a single attachment from DFIRe removes both the database row and the backing object on the bound target.

Deleting a case removes all its attachments along with their backing objects on whichever storage location holds them — for both tiers, regardless of how the file got there (managed upload or discovered via Sync).

Switching a role's target

You can change a role's target at any time. Existing attachments keep being read from the target they were originally written to — DFIRe stamps each attachment with its target at write time so the binding survives later role changes.

If you delete a target that has historical attachments, those attachments stay in the database but become "orphaned": the row remains so you can clean up metadata or delete it from DFIRe, but downloads return HTTP 410 with an explanatory message until either the target is recreated or the row is deleted. The Direct Storage row UI shows an "Orphaned — target removed" badge in this state.

Test workflow

Three test buttons cover the common configuration paths:

Where What it does
Test connection — on the target form Full round-trip against the backend: write a small test object, read it back, decrypt + hash-compare (for Local / S3 / SMB), delete. Confirms credentials, network reachability, and (for the Encrypted-tier paths) that DFIRe's encryption layer round-trips correctly. For SFTP, also captures the host key fingerprint on first run.
Test Direct — on the Direct Storage role row Browser-side path test: CORS preflight + signed PUT (S3), or the passthrough chunk endpoint (SMB / SFTP). Surfaces CORS misconfiguration, browser-origin mismatch, and other issues that wouldn't show up in the basic backend test.
Last-test badge — on the target list Snapshot of the most recent test result + when it ran. Green if the test passed in the last 5 minutes, amber under a day, red over a day or on the most recent failure. Successful uploads / downloads against the target also bump the badge timestamp, so a target in active use stays green without re-clicking Test.

Always run Test connection before binding a target to a role in production. The test exercises the same code path real uploads use, so catching a misconfiguration here prevents the first user upload from failing.

Quotas

Each target has an optional storage quota in GB, enforced at the application level. When a target's quota is reached, new uploads to roles bound to that target are rejected with a clear error; existing files remain accessible. Deleting files frees up quota space.

Current usage is displayed alongside each target on the Storage Targets page. The Direct Storage section header on each case page also shows live usage / remaining quota for the bound Direct Storage target so users can see capacity at a glance before starting a multi-GB upload.

Two backend-side concurrency caps protect the application server from being overrun by simultaneous uploads:

  • Per user: 10 simultaneous Direct Storage upload sessions. Hitting this returns 429 + Retry-After; the browser auto-retries and the row stays in "queued" state until a slot frees.
  • Globally across all users: 20 simultaneous chunk-write sessions. Same 429 / queue handshake.

Both caps are tunable via environment variables (MAX_CONCURRENT_DIRECT_UPLOADS for per-user, MAX_CONCURRENT_DIRECT_UPLOADS_GLOBAL for the system-wide cap). Default values are sized for typical small-team forensics workloads.

Audit trail

Every storage operation lands in the audit log. Filter the audit log by these action codes to reconstruct what happened to a particular file:

Action When it fires
CREATE / DELETE Encrypted Storage attachment created or removed. Standard audit shape; same as any other DFIRe object.
DOWNLOAD Any attachment downloaded. Annotated with delivery method and tier.
DIRECT_UPLOAD_INIT Direct Storage upload session started. Includes filename, size, target, and the uploader.
DIRECT_UPLOAD_COMPLETE All parts received; attachment row created in verifying state.
DIRECT_UPLOAD_ABORT Session cancelled by the user, the orphan-sweep, or a backend failure during init.
DIRECT_HASH_BROWSER_DECLARED The uploader's browser declared a SHA-256 with the upload completion. Marks the start of the trust chain for browser-attested hashes.
DIRECT_HASH_VERIFIED Server-side hash verification matched the client-declared hash.
DIRECT_HASH_MISMATCH Server-side verification disagreed with the client-declared hash. The backing object is removed (so it can't be re-imported via Sync) and the row is marked as error.
DIRECT_HASH_DISK_MISMATCH Re-verify on a row with a previously-stored hash found the disk content disagrees. The file is preserved and the row is marked "Disk mismatch" so the user investigates.
DIRECT_SYNC Bucket reconciliation run on a case or item. Includes the lists of added / removed / unchanged filenames plus any in-flight uploads that were skipped.
SHA256_COMPUTED A user-triggered Calculate or Re-verify finished successfully. Includes the recorded hash, the bytes-hashed count, and the size+mtime snapshot taken at hash time.
SHA256_FAILED A Calculate / Re-verify run failed (network error, file disappeared, etc.). Includes the failure reason.
SHA256_INVALIDATED Sync detected the backend file's size or mtime has changed since the last hash; the row is flipped to "Stale". Old hash preserved for forensic record.
SHA256_CANCELLED The user cancelled an in-flight hash compute. Records how many bytes were hashed before the cancel.
ATTACHMENT_BACKEND_CLEANUP_FAILED An attachment row was deleted but the backing object cleanup against the storage backend failed (network, permission, etc.). The DB row is gone; this audit row holds the target ID and object path so an operator or the orphan-sweep can reconcile later.

Encryption keys

The Encrypted Storage tier uses AES-256-GCM with a three-layer key hierarchy (tenant key → entity key → per-file key). All files are encrypted before they leave the application server and cannot be read without the keys, regardless of who has access to the storage backend.

See Application Security for the encryption architecture and key-management details.

Critical: Back up your CREDENTIAL_ENCRYPTION_KEY environment variable immediately after deployment. If this key is lost, encrypted files cannot be recovered regardless of which storage backend you use.