Skip to content
Kutrion
Browse docs

managed-file-transfer

Managed file transfer

How Kutrion moves files: the protocols it reaches, server-to-server copies through the broker, durable transfer jobs, schedules and folder watchers, the DLP and bandwidth policies on the path, and the exact download contract a client has to code against.

Updated Aug 28, 2026

Kutrion moves files over the same broker that already holds the credentials. A copy between two remotes runs server-side: the connection’s secret is decrypted in memory for that one authorized action, the bytes stream source to destination through the server, and neither side’s credential is ever handed to the client. What the browser gets is a progress number and an audit record.

This page describes the transfer surface as it is built. Where a capability has an edge — a size cap, a policy that only bites when an operator configures it, a thing we deliberately do not do — the edge is here rather than left to be found.

What it can reach

Two families of connection, plus OAuth cloud accounts.

FamilyProtocolsWhere the credential comes from
SSH connectionsSFTPThe same connection profile the terminal uses
File connectionsFTP, FTPS, WebDAV, S3-compatible, Azure Blob, OpenStack SwiftA vault credential referenced by the connection
Cloud accountsDropbox, OneDrive, Google Drive, BoxAn OAuth grant, sealed at rest

Cloud accounts are only offered once an operator has set that provider’s client id and secret; providers without configured app credentials stay hidden rather than failing at the authorize step.

Every destination a connection is composed from — the host for FTP/FTPS/WebDAV, the composed service URL for S3, Azure and Swift — is resolved and checked against the egress guard before the adapter dials it. A tenant-supplied host cannot be pointed at an internal service or a cloud metadata endpoint and read back through list or download.

Moving files

Interactive. A copy or a directory sync between two connections runs immediately and reports what it did. Sync diffs both trees and can run one-way or two-way, restrict itself to newer files, filter with include/exclude globs, and preview as a dry run before anything is written. Files present only on the destination are reported as extra; they are deleted only when you ask for a mirror, and a two-way sync never deletes at all.

Unattended. Anything large, flaky or scheduled should be a transfer job instead. A job is a persisted row: it survives you closing the tab, it retries on its own, and it verifies what arrived.

Transfer jobs

A job holds a source, a destination, and options. It is queued rather than run on the spot, and the client polls for status.

  • Retries. Three attempts by default, with exponential backoff — 1s, 2s, 4s, doubling to a 30-second ceiling. Errors that will fail identically every time (a policy block, a missing path, a permission refusal, a bad credential) are treated as terminal and do not burn the remaining attempts.
  • Integrity. For a single file, the destination is re-read and hashed with SHA-256 and compared to the source. A mismatch fails the attempt rather than reporting success. Files above 2 GB are not re-read just to verify; the job records the integrity result as skipped rather than claiming a verification it did not perform.
  • Fan-out. One source can be delivered to up to ten extra destinations. Any delivery failing aborts the attempt, so a retry re-delivers to all of them — deliveries overwrite, so that is idempotent.
  • Post-transfer action on the source. Move it into an archive folder, or delete it. Applied only once every delivery has succeeded, never on a partial run.
  • Queue control. Jobs carry a priority (higher runs first, oldest first within a priority) and can depend on another of your jobs, waiting until that one succeeds. A job whose dependency fails or is cancelled is cancelled itself with dependency_failed. A queued job can be reprioritised or cancelled; once it is running or finished it cannot.
  • Concurrency. A global cap (three concurrent jobs by default) governs how many run at once.
  • Crash recovery. A running job carries a heartbeat and a lease. If the process running it dies, the job is requeued once the lease lapses and nothing has touched the row for several heartbeats — and a job a live process still owns is never taken away from it.
  • Audit. Creation, cancellation, completion (with file count, bytes and integrity result) and failure are all written to the audit trail. The failure event is the one to alert on.

Single transfers are capped at 20 GB per file by default; a self-hosted operator can change that.

Schedules and folder watchers

A schedule fires a job on a cadence: either every N seconds, or at a fixed minute of the day in UTC. The sweep runs once a minute. A schedule that cannot be advanced does not fire — that ordering is deliberate, because a schedule that stays due would otherwise enqueue a duplicate transfer on every tick.

A folder watcher polls a source directory — every five minutes by default, never more often than once a minute — and delivers new or changed files on its own. Files are filtered by filename glob, by minimum and maximum size, and by minimum age. With stability checking on — the default posture — a file must have been seen at the same size on a previous poll before it fires, so a file still being written is held back. A file that is later rewritten at a different size fires again. Each matching file becomes its own transfer job, so retries, integrity checking and audit all apply.

Workflow templates

A routine can be saved as a versioned template: a job spec with {{placeholder}} parameters filled in at instantiation. Versions are immutable revisions and instantiating always names one, so a run is reproducible.

A template can never carry a shell command. The only post-transfer action it accepts is the move-or-delete of the source, validated field by field, and any key outside the known set is rejected rather than ignored — an ignored key is an executed key the first time someone adds a generic passthrough.

Policy on the path

DLP. A per-organization policy maps filename globs to allow, redact or block, with a default action and an optional secrets-severity floor. It is evaluated on upload, on transfer, on a share link’s redemption and on a guest drop. Precedence is worth knowing: a block rule wins outright, then the secrets floor, then the first matching rule, then the default. The floor sits above the rules on purpose — an allow rule naming a file cannot buy it a weaker verdict than the unnamed file beside it would get. Content is only read when the verdict depends on it. When a policy demands inspection and the file cannot be inspected — binary where redaction was required, or larger than the scan can read — the file is refused rather than passed through unexamined.

Bandwidth and size. A per-organization policy sets a per-transfer byte/sec ceiling, optional time-of-day windows that tighten it (weekday business hours, overnight, and windows that wrap past midnight), a cap on how many transfers run concurrently, and a maximum upload size enforced against the declared Content-Length before a byte is streamed.

Malware scanning and content inspection. These are dormant until an operator configures them, and no deployment changes behaviour until one does. Where a ClamAV daemon is configured, uploads are buffered up to the scan cap and scanned before they are written; an infected file is refused with 422 and recorded in a quarantine ledger that holds metadata only — path, signature, engine, size, hash — never contents. The behaviour when the scanner itself is unreachable is a deliberate choice: the default is fail-open (the upload proceeds) and the audit record says failed_open rather than claiming the file was examined. An operator who needs the stricter posture sets fail-closed. Content disarm and reconstruction, sandbox detonation and threat-intel reputation hang off the same buffering hook and are dormant by the same rule.

Note the interaction: uploads normally stream straight through to the remote without landing in server memory, but a DLP rule that needs content, or any configured inspection engine, buffers the file up to that engine’s cap. A file over the cap is refused with 413 rather than written unexamined.

Network access policy. An organization can restrict the authenticated file routes by source IP, CIDR or country before any of them run; a refusal is a 403 blocked_by_access_policy and is audited. Country rules only bite where a geo-aware proxy sets the configured country header. This policy is applied to the authenticated /files routes — it does not gate public share links or guest drop zones, which have their own limits described below.

Download watermarking, precisely

An organization can enable a download watermark for leak attribution. What it promises is best-effort attribution for ordinary text downloads — it works, and it is narrower than the name suggests. The conditions are set out here so you can predict, before you turn it on, whether a given file will be stamped.

What a stamp is. A delimiter line above and below the file’s contents carrying the organization, the user, a UTC timestamp and a per-download id, rendered from a template the organization edits. The same id is written to the download’s audit record, so a copy that leaks can be traced back to the download that produced it. Every download through the broker carries that id whether or not a banner was applied.

A file is stamped only when all of the following hold. If any one of them does not, the file is downloaded unaltered — the download still happens, and is still audited.

  • It is served by the broker. File connections (FTP, FTPS, WebDAV, S3-compatible, Azure Blob, Swift), SFTP downloads on an SSH connection, cloud-account downloads (Dropbox, OneDrive, Google Drive, Box) and public share links all consult the policy. A presigned object-store link does not: that download goes straight to the bucket with no broker in the path, so it is neither stamped nor recorded — only the issuing of the link is.
  • The filename ends in a recognised text extension. The list is exactly: .txt .log .csv .tsv .json .ndjson .xml .yaml .yml .md .markdown .ini .conf .cfg .env .properties .sql .html .htm .css .js .ts .sh .bash .py .rb .go .java .c .h .cpp .rs .toml. The decision is made on the name and nothing else, so a text file renamed to .bin, or one whose extension is simply not on the list, is not stamped. Anyone who can download a file can rename it first.
  • It is smaller than 50MB. Stamping requires the whole file in server memory. Above that limit the attempt is abandoned and the file streams unstamped.
  • Its contents are not binary. A file with a text extension whose leading bytes contain a NUL is treated as binary and sent unaltered — the extension decides whether to try, the contents decide whether to finish.
  • The request is for the whole file, from offset zero. A file this policy would stamp has one representation the server will hand over — the stamped whole — so a ranged or resumed request for it is refused rather than served unstamped.

There is no rendering-aware watermarking. PDFs, images and office documents are downloaded unaltered. Marking those needs a renderer per format, and that is not built.

What the audit trail tells you. A stamped download is recorded as watermarked. One that was not stamped is recorded without that marker, and the record does not say which of the conditions above was the reason.

Treat it as attribution for text artefacts, not as a control that marks every file leaving the organization.

Sending and receiving outside the organization

Share links. A share streams one remote file through the broker to whoever holds the link, so the recipient never touches the connection or its credential. A link carries an expiry (up to 7 days) and a view cap (one download by default), and can be bound to a recipient — in which case a six-digit one-time code is emailed to them and the link alone is not enough. Failed code entries are capped per share, so rotating IP addresses does not extend the guess budget, and a locked drop has to be re-shared. The organization’s DLP policy governs what may leave through a link exactly as it governs what may be written.

Every refusal past the point where a download is claimed — expired, capped, locked, blocked by policy, connection revoked — is byte-identical to the recipient. That is deliberate: distinguishing them would confirm to an anonymous holder that a link is live and leak what the file’s name or content matched. The reason goes to the organization’s audit trail, which is where the people who need it are.

Drop zones. A file request publishes a link that lets someone without an account upload into exactly one folder. The guest-supplied filename is reduced to a safe basename — any directory part or traversal is stripped — so the write cannot be redirected. A drop carries an expiry, an upload count cap and a byte cap, the upload slot is claimed atomically so concurrent drops cannot exceed the cap, and a broker failure rolls the slot back rather than burning it. The DLP policy is enforced on the guest path too. Each accepted upload produces a receipt with the filename, size and SHA-256.

The download contract

If you are writing a client against the download routes — GET /files/connections/:id/download and GET /ssh/connections/:id/sftp/download — these are the responses to code against. Both routes answer alike.

Resuming. Send Range: bytes=<start>-. The response is 206 Partial Content with Content-Range: bytes <start>-<size-1>/<size> and a Content-Length of the remainder. Only the start offset is honoured: an end value in the header is ignored and a suffix range (bytes=-500) is not recognised, so either returns the whole file with 200.

A range at or past the end of the file answers 416.

HTTP/1.1 416 Range Not Satisfiable
Accept-Ranges: bytes
Content-Range: bytes */12345

{"error":"range_not_satisfiable","size":12345}

A resuming client must read this as “you already have the whole file”, not as a transfer failure. It is the honest answer to a request with no satisfiable response, and the total size is the only thing a 416 can truthfully carry. Requesting bytes=<size>- — exactly what a client that has finished sends when it retries — is the common way to reach it. No FILE_DOWNLOAD audit record is written, because a range with no answer is not a download attempt.

A missing path answers 404.

HTTP/1.1 404 Not Found

{"error":"path_not_found","detail":"..."}

A path that is not there is the caller’s answer, not a broker outage. A genuine transport failure — a dead session, an unreachable remote — is still a 502, with file_op_failed on the file routes and sftp_failed on the SFTP ones. Branch on the status, not on the detail string, which is written for a human to read.

Other codes on these routes: 400 path_required, 403 for a policy or permission refusal, 413 file_too_large when a declared upload exceeds the organization’s cap, 415 not_text when redaction is asked for on a binary file, and 422 blocked_by_malware_scan.

What Kutrion does not do

  • Resumable upload. There is none. A browser upload streams straight through to the remote, and if the connection drops you start it again. Resuming an upload is a session protocol, not a request parameter, and we would rather say so than imply one. The answer for anything large or unattended is a transfer job: it runs server-side, survives the client, retries on its own and verifies the destination hash. Downloads can be resumed, per the contract above.
  • Cloud accounts as unattended endpoints. A transfer job, a schedule and a folder watcher take an SSH connection or a file connection as their source and destination. An OAuth cloud account is not one of those, so it cannot be a scheduled or watched endpoint.
  • Shell commands in a workflow. Deliberately absent, for the reason given above.